【Linux】进程间通信——system V 共享内存、消息队列、信号量_信号量共享内存-程序员宅基地

技术标签: Linux  linux  服务器  

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

写在前面

在上一篇文章中我们讲了一种进程间通信的方式管道,管道通信的本质是基于文件的,也就是说OS没有为此做过多的设计,但是system V IPC是操作系统特地设置的一种通信方式。提供的通信方式有以下三种

  • system V 共享内存
  • system V 消息队列
  • system V 信号量

1. 共享内存

1.1 共享内存的概念

我们在上一篇文章中讲到:要实现进程间通信,就一定要让不同的进程看到同一份资源

在匿名管道/命名管道中,我们让不同进程看到同一份资源的方式是让不同进程打开同一个文件,使用对文件的读写来实现进程间通信,那么除此之外,我们还有其他方法

让不同进程能够使用同一块物理内存,就是共享内存的核心思想

1.2 共享内存的原理

由于进程具有独立性,内核数据结构包括对应的代码、数据和页表都是独立的,为了实现进程间通信需要以下过程:

1. OS申请一段空间

2. 将申请好的物理内存空间映射到一个进程地址空间

3. 将同一块物理内存空间映射到另一个需要通信的进程中

4. 通信结束之后取消进程和物理内存的映射关系,然后释放内存

  • 我们把OS申请的空间叫做共享内存
  • 进程和共享内存建立映射关系叫做挂接
  • 取消进程和共享内存之间的映射关系叫做去关联
  • 释放内存叫做释放共享内存

image-20240119223153670

对共享内存的理解

在C语言中,我们可以使用malloc在物理内存上申请空间,并把申请的空间经过页表映射到进程地址空间中,返回进程地址空间的指定地址,但是对于共享内存的通信方式,需要被专门设计。因为在同一时间,可能会有很多进程需要使用这种方式进行通信,所以一定会同时存在很多的共享内存,所以需要被管理起来,因此需要被专门设计

1.3 共享内存的使用

1.3.1 创建

我们使用shmget系统调用来创建共享内存

image-20240119224359785

头文件: 
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型: int shmget(key_t key, size_t size, int shmflg);
参数解释:
    key:是一个保证共享内存编号唯一性的标识符,为了让相同的进程能够看到同一个共享内存
    size:创建的共享内存的大小
    shmflg:创建共享内存的选项,通常我们使用两个:IPC_CREAT和IPC_EXCL
返回值:如果调用成功就返回一个合法的共享内存描述符shmid,如果调用失败就返回-1同时设置错误码

shmflg的选项含义:

  • IPC_CREAT:如果对应key的共享内存不存在就创建,如果存在就获取对应的shmid
  • IPC_EXCL:这个选项不能单独使用,和IPC_CREAT配合使用,如果不存在就创建,存在就出错返回

key的形成方式

我们使用一个特定的函数ftok来形成一个唯一的key

image-20240119225546733

头文件:
#include <sys/type.h>
#include <sys/ipc.h>
函数原型:
key_t ftok(const char *pathname, int proj_id);
参数解释:
	pathname:这是一个指向用于生成键值的路径名的C字符串指针。通常会选择一个已经存在的文件作为这个路径名,因为它可以确保唯一性。通常情况下,可以选择程序中的某个文件作为路径名,这样就可以确保不同的程序使用不同的路径名生成不同的键值。
	proj_id:这是一个整数值,用于进一步区分不同的 IPC 对象。这个值在给定路径名的范围内必须唯一。通常情况下,可以使用与程序相关的整数值作为 proj_id,以确保不同的程序使用不同的 proj_id 生成不同的键值。
返回值: 如果调用成功就返回对应的key值,调用失败就返回-1,同时设置错误码

对key和shmid的理解

key是在OS层面的,给OS看的标定共享内存的标识符,shmid是应用层的,是给我们看的,标定共享内存的标识。key和shmid的关系就像是inode和fd的关系

举个例子:

/*comm.hpp*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cerrno>
#include <iostream>

#define PATHNAME "."  // 使用当前目录作为项目目录
#define PROJ_ID 0x66  // 随机的项目id
#define MAX_SIZE 4096 // 创建的共享内存大小

key_t getKey() // 封装获取key的函数
{
    
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k == -1)
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

int getShmHelper(key_t key, int flags) // 封装通过key来获取shmid的函数
{
    
    int shmid = shmget(key, MAX_SIZE, flags);
    if(shmid < 0)
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int getShm(key_t key) // 用于找到已经创建的共享内存的shmid,所以传入的选项只有IPC_CREAT,不关心以前是否创建
{
    
    return getShmHelper(key, IPC_CREAT);
}

int createShm(key_t key) // 用于创建,所以传入的选项中有IPC_EXCL,表示如果遇到冲突就创建失败
{
    
    return getShmHelper(key, IPC_CREAT | IPC_EXCL | 0666); // 0666表示创建的共享内存的权限
}
/*server.cc*/
#include "comm.hpp"

int main()
{
    
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = createShm(k);
    printf("%d\n", shmid);
    return 0;
}

/*client.cc*/
#include "comm.hpp"

int main()
{
    
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = getShm(k);
    printf("%d\n", shmid);
    return 0;
}

image-20240119234453939

1.3.2 控制

上述的代码编译出来的程序第一次运行没有任何问题,但是如果再次运行server就会发现:

image-20240119234652904

这是因为创建的共享内存没有被释放,所以我们在使用完共享内存后需要释放

补充:查看IPC资源

我们可以通过命令ipcs系列指令来查看进程间通信相关信息

image-20240120182028480

  • 删除共享内存

    ipcrm -m + shmid

    image-20240120182434821

当然除了命令删除之外,还可以使用系统调用来对共享内存进行删除/控制:shmctl

image-20240120183057685

头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数解释:
	shmid:要控制的共享内存的shmid
	cmd:要执行的命令,包括:IPC_STAT,IPC_SET,IPC_RMID,IPC_INFO,SHM_INFO,SHM_STAT,SHM_LOCK,SHM_UNLOCK.这里我们先不关注其他的,只关注IPC_RMID指令,这个指令是用来释放对应的shmid的
	buf:在其他指令中,有一些是需要获取到一些信息的,buf作为输出型参数来保存相关信息
返回值:对于释放共享内存来说,0表示成功,-1表示失败

使用shell脚本监视共享内存的情况

while :; do ipcs -m ; echo "##########################################"; sleep 1; done
#include "comm.hpp"

int main()
{
    
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = createShm(k);
    printf("%d\n", shmid);

    sleep(5);
    shmctl(shmid, IPC_RMID, nullptr); // 这里不需要获取信息,传入nullptr即可
    return 0;
}

image-20240120184045003

1.3.3 关联

在本节开始,我们说过有一个过程叫做把物理内存和进程地址空间关联起来,我们会使用一个系统调用关联:shmat,这里的at取attach的意义

image-20240120184534528

头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数解释:
	shmid:需要关联的shmid
	shmaddr:关联到进程地址空间的地址,我们绝大多数时间是不指定的,所以传nullptr即可
	shmflg:关联的选项,默认为0,表示读写权限
返回值:
	关联成功返回共享内存映射到进程地址空间中的起始地址,失败返回-1并设置错误码
void *attachShm(int shmid)
{
    
    void *mem = shmat(shmid, nullptr, 0);
    if((long long)mem == -1L) // 这里由于我们的机器是64位的,所以一个地址占8个字节,所以需要转成long long类型判断是否正确关联
    {
    
        std::cout << errno << " : " << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

1.3.4 去关联

有关联,那么对应的就有去关联的操作,去关联使用的系统调用是shmdt

image-20240120185829638

头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
int shmdt(const void *shmaddr);
参数解释:
	shmaddr:需要去关联的进程地址空间
返回值:
	如果调用成功就返回0,否则就返回-1,同时设置错误码
void detachShm(void* start)
{
    
    if(shmdt(start) == -1)
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }
}

1.3.6 实战——实现server和client端的通信

/*comm.hpp*/
#pragma once
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <iostream>

#define PATHNAME "."  // 使用当前目录作为项目目录
#define PROJ_ID 0x66  // 随机的项目id
#define MAX_SIZE 4096 // 创建的共享内存大小

key_t getKey() // 封装获取key的函数
{
    
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k == -1)
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

int getShmHelper(key_t key, int flags) // 封装通过key来获取shmid的函数
{
    
    int shmid = shmget(key, MAX_SIZE, flags);
    if(shmid < 0)
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int getShm(key_t key) // 用于找到已经创建的共享内存的shmid,所以传入的选项只有IPC_CREAT,不关心以前是否创建
{
    
    return getShmHelper(key, IPC_CREAT);
}

int createShm(key_t key) // 用于创建,所以传入的选项中有IPC_EXCL,表示如果遇到冲突就创建失败
{
    
    return getShmHelper(key, IPC_CREAT | IPC_EXCL | 0666); // 0666表示创建的共享内存的权限
}

void delShm(int shmid)
{
    
    if(shmctl(shmid, IPC_RMID, nullptr) == -1)// 这里不需要获取信息,传入nullptr即可
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }
}

void *attachShm(int shmid)
{
    
    void *mem = shmat(shmid, nullptr, 0);
    if((long long)mem == -1L) // 这里由于我们的机器是64位的,所以一个地址占8个字节,所以需要转成long long类型判断是否正确关联
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

void detachShm(void* start)
{
    
    if(shmdt(start) == -1)
    {
    
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }
}
/*server.cc*/
#include "comm.hpp"

int main()
{
    
    key_t k = getKey();                     // 通过共同的pathname和proj_id构建一个相互通信的进程之间的key
    int shmid = createShm(k);               // 通过创建的key创建一段共享内存
    char *start = (char *)attachShm(shmid); // 将这段共享内存和当前进程地址空间关联

    // 使用共享内存通信
    while (true)
    {
    
        std::cout << "client say# " << start << std::endl; // 这里可以直接读取通信信息,因为地址相同
        struct shmid_ds ds;
        shmctl(shmid, IPC_STAT, &ds); //    获取shmid的相关信息
        printf("获取属性:size:%d,pid:%d,myself:%d", ds.shm_segsz, ds.shm_cpid);
        sleep(1);
    }

    delShm(shmid); // 使用完之后去关联
    delShm(shmid); // 谁创建的共享内存谁来释放
    return 0;
}
/*client*/
#include "comm.hpp"

int main()
{
    
    key_t k = getKey();                     // 通过共同的pathname和proj_id构建一个相互通信的进程之间的key
    int shmid = getShm(k);                  // 通过创建的key获取指定的共享内存
    char *start = (char *)attachShm(shmid); // 将这段共享内存和当前进程地址空间关联

    // 使用共享内存通信
    const char *message = "hello server,我是另一个进程,正在和你通信"; // 通信信息
    pid_t id = getpid();
    int count = 1;
    while (true)
    {
    
        sleep(5);
        snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, count++); // 直接讲通信信息写到start中即可
    }

    delShm(shmid); // 使用完之后去关联
    return 0;
}

image-20240120191913957

1.3.7 共享内存的特点

共享内存的生命周期是随OS的,而不是随进程的,这是所有System V进程间通信的共性

共享内存的优点:共享内存是所有进程间通信速度是最快的,因为共享内存是被双方所共享,只要写入对方就能立即看到,能大大减少数据的拷贝次数。

但是综合考虑管道和共享内存,考虑键盘输入,和显示器输出,对于同一份数据:共享内存有几次数据拷贝,管道有几次数据拷贝

管道:需要通过键盘输入到自己定义的缓冲区char buffer[],将数据拷贝到buffer中,调用write接口在把buffer里的数据拷贝到管道里,

另一进程也有定义buffer缓冲区,调用read读取把数据从管道里读取到buffer里,在把数据显示到显示器上:

img

共享内存:通过映射关系

img

共享内存的缺点:不给我们进行同步和互斥的操作,没有对数据做任何保护。客户端和服务端没做保护,如果想做保护要用到信号量,对共享内存进行保护,写完通过读端进行读取。

2. 消息队列(不常用)

2.1 消息队列的概念

消息队列是OS提供的内核级队列,消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

 struct msqid_ds {
    
     struct ipc_perm msg_perm;     /* Ownership and permissions */
     time_t          msg_stime;    /* Time of last msgsnd(2) */
     time_t          msg_rtime;    /* Time of last msgrcv(2) */
     time_t          msg_ctime;    /* Time of last change */
     unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
     msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
     msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
     pid_t           msg_lspid;    /* PID of last msgsnd(2) */
     pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
 };

消息队列数据结构的第一个成员是msg_perm,它和shm_perm是同一个类型的结构体变量,ipc_perm结构体的定义如下:

struct ipc_perm {
    
    key_t          __key;       /* Key supplied to msgget(2) */
    uid_t          uid;         /* Effective UID of owner */
    gid_t          gid;         /* Effective GID of owner */
    uid_t          cuid;        /* Effective UID of creator */
    gid_t          cgid;        /* Effective GID of creator */
    unsigned short mode;        /* Permissions */
    unsigned short __seq;       /* Sequence number */
};

2.2 消息队列的使用

2.2.1 获取消息队列

msgget

image-20240120192645998

2.2.2 控制消息队列

msgctl

image-20240120192711808

2.2.3 发送和接收数据

msgsndmsgrcv

image-20240120192752479

3. 信号量

关于信号量的知识,我们将会在后面多线程的地方详细讲解,这里先进行一些概念的扫盲

3.1 信号量的概念扫盲

  • 信号量的本质是一个计数器**,通常用来表示公共资源中,资源数的多少问题。信号量主要用于同步和互斥的。

  • 公共资源:能被多个进程同时访问的资源,访问没有保护的公共资源可能会导致数据不一致问题。要让不同的进程看到同一份资源是为了通信,通信是为了让进程间实现协同,而进程之间具有独立性,所以为了解决独立性问题要让进程看到同一份资源,但是会导致数据不一致的问题。

  • 临界资源:被保护起来的公共资源

  • 临界区:进程要使用资源一定是该进程有对应的代码来访问这部分临界资源,这段代码就是临界区,但是多个进程看到同一份资源是少数情况,大部分申请自己的资源用自己的代码区访问。

  • 非临界区:不访问公共资源的代码。

如何保护公共资源:互斥&&同步

互斥:由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥

原子性:要么不做、要么做完两态的这种情况。比如支付转账

如果用全局的整数来替代信号量?

全局的整数在父子关系的进程上都看不到,要发生写时拷贝,而不同的进程更看不到,所以进程间想看到同一个计数器得让进程看到同一个计数器。

为什么要信号量?

当我们想要某种资源的时候可以通过信号量进行预;,共享资源被使用的方式:作为一个整体使用;划分成为一个一个的资源部分

3.2 内核中信号量的相关数据结构

struct semid_ds {
    
    struct ipc_perm sem_perm;  /* Ownership and permissions */
    time_t          sem_otime; /* Last semop time */
    time_t          sem_ctime; /* Last change time */
    unsigned long   sem_nsems; /* No. of semaphores in set */
};

信号量数据结构的第一个成员也是ipc_perm类型的结构体变量,ipc_perm结构体的定义如下:

struct ipc_perm {
    
    key_t          __key; /* Key supplied to semget(2) */
    uid_t          uid;   /* Effective UID of owner */
    gid_t          gid;   /* Effective GID of owner */
    uid_t          cuid;  /* Effective UID of creator */
    gid_t          cgid;  /* Effective GID of creator */
    unsigned short mode;  /* Permissions */
    unsigned short __seq; /* Sequence number */
};

3.3 信号量的PV操作

我们知道信号量本质上就是一个临界资源的计数器,有进程要使用这个临界资源,就会导致可用的临界资源数量减少,使用完之后归还临界资源会导致可用的临界资源增多

  • P操作:向OS预定临界资源,会导致现有临界资源减少
  • V操作:向OS归还临界资源,会导致现有临界资源增加

假设信号量为sem,那么P操作相当于sem++,V操作相当于sem--,注意这里的++ 和 - -操作都是原子的

如果信号量的初始值是1就代表了访问公共资源作为一个整体来使用。二元信号量提供互斥功能

3.4 信号量相关函数

3.4.1 申请信号量

semget

image-20240120194422012

3.4.2 控制信号量

semctl

image-20240120194438064

3.4.3 信号量的操作PV

semop

image-20240120194454493


关于system V标准的进程间通信的思考

我们可以发现,共享内存、消息队列、信号量接口相似度非常高,获取与删除,都是system V标准的进程间通信。

OS如何管理:先描述,在组织,对相关资源的内核数据结构做管理,对于共享内存、消息队列、信号量的第一个成员都是ipc_perm:

struct ipc_perm {
    
    key_t          __key;    /* Key supplied to shmget(2) */
    uid_t          uid;      /* Effective UID of owner */
    gid_t          gid;      /* Effective GID of owner */
    uid_t          cuid;     /* Effective UID of creator */
    gid_t          cgid;     /* Effective GID of creator */
    unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
    unsigned short __seq;    /* Sequence number */
};

虽然内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量,都可以通过key来标识唯一性。这样设计的好处:在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。((struct shmid_ds*)perms[0],强转,此时就可以访问其他剩下的属性)


本节完…

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_63249832/article/details/135720762

智能推荐

hdu 1229 还是A+B(水)-程序员宅基地

文章浏览阅读122次。还是A+BTime Limit: 2000/1000 MS (Java/Others)Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 24568Accepted Submission(s): 11729Problem Description读入两个小于10000的正整数A和B,计算A+B。...

http客户端Feign——日志配置_feign 日志设置-程序员宅基地

文章浏览阅读419次。HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息。FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。BASIC:仅记录请求的方法,URL以及响应状态码和执行时间。NONE:不记录任何日志信息,这是默认值。配置Feign日志有两种方式;方式二:java代码实现。注解中声明则代表某服务。方式一:配置文件方式。_feign 日志设置

[转载]将容器管理的持久性 Bean 用于面向服务的体系结构-程序员宅基地

文章浏览阅读155次。将容器管理的持久性 Bean 用于面向服务的体系结构本文将介绍如何使用 IBM WebSphere Process Server 对容器管理的持久性 (CMP) Bean的连接和持久性逻辑加以控制,使其可以存储在非关系数据库..._javax.ejb.objectnotfoundexception: no such entity!

基础java练习题(递归)_java 递归例题-程序员宅基地

文章浏览阅读1.5k次。基础java练习题一、递归实现跳台阶从第一级跳到第n级,有多少种跳法一次可跳一级,也可跳两级。还能跳三级import java.math.BigDecimal;import java.util.Scanner;public class Main{ public static void main(String[]args){ Scanner reader=new Scanner(System.in); while(reader.hasNext()){ _java 递归例题

面向对象程序设计(荣誉)实验一 String_对存储在string数组内的所有以字符‘a’开始并以字符‘e’结尾的单词做加密处理。-程序员宅基地

文章浏览阅读1.5k次,点赞6次,收藏6次。目录1.串应用- 计算一个串的最长的真前后缀题目描述输入输出样例输入样例输出题解2.字符串替换(string)题目描述输入输出样例输入样例输出题解3.可重叠子串 (Ver. I)题目描述输入输出样例输入样例输出题解4.字符串操作(string)题目描述输入输出样例输入样例输出题解1.串应用- 计算一个串的最长的真前后缀题目描述给定一个串,如ABCDAB,则ABCDAB的真前缀有:{ A, AB,ABC, ABCD, ABCDA }ABCDAB的真后缀有:{ B, AB,DAB, CDAB, BCDAB_对存储在string数组内的所有以字符‘a’开始并以字符‘e’结尾的单词做加密处理。

算法设计与问题求解/西安交通大学本科课程MOOC/C_算法设计与问题求解西安交通大学-程序员宅基地

文章浏览阅读68次。西安交通大学/算法设计与问题求解/树与二叉树/MOOC_算法设计与问题求解西安交通大学

随便推点

[Vue warn]: Computed property “totalPrice“ was assigned to but it has no setter._computed property "totalprice" was assigned to but-程序员宅基地

文章浏览阅读1.6k次。问题:在Vue项目中出现如下错误提示:[Vue warn]: Computed property "totalPrice" was assigned to but it has no setter. (found in <Anonymous>)代码:<input v-model="totalPrice"/>原因:v-model命令,因Vue 的双向数据绑定原理 , 会自动操作 totalPrice, 对其进行set 操作而 totalPrice 作为计..._computed property "totalprice" was assigned to but it has no setter.

basic1003-我要通过!13行搞定:也许是全网最奇葩解法_basic 1003 case 1-程序员宅基地

文章浏览阅读60次。十分暴力而简洁的解决方式:读取P和T的位置并自动生成唯一正确答案,将题给测点与之对比,不一样就给我爬!_basic 1003 case 1

服务器浏览war文件,详解将Web项目War包部署到Tomcat服务器基本步骤-程序员宅基地

文章浏览阅读422次。原标题:详解将Web项目War包部署到Tomcat服务器基本步骤详解将Web项目War包部署到Tomcat服务器基本步骤1 War包War包一般是在进行Web开发时,通常是一个网站Project下的所有源码的集合,里面包含前台HTML/CSS/JS的代码,也包含Java的代码。当开发人员在自己的开发机器上调试所有代码并通过后,为了交给测试人员测试和未来进行产品发布,都需要将开发人员的源码打包成Wa..._/opt/bosssoft/war/medical-web.war/web-inf/web.xml of module medical-web.war.

python组成三位无重复数字_python组合无重复三位数的实例-程序员宅基地

文章浏览阅读3k次,点赞3次,收藏13次。# -*- coding: utf-8 -*-# 简述:这里有四个数字,分别是:1、2、3、4#提问:能组成多少个互不相同且无重复数字的三位数?各是多少?def f(n):list=[]count=0for i in range(1,n+1):for j in range(1, n+1):for k in range(1, n+1):if i!=j and j!=k and i!=k:list.a..._python求从0到9任意组合成三位数数字不能重复并输出

ElementUl中的el-table怎样吧0和1改变为男和女_elementui table 性别-程序员宅基地

文章浏览阅读1k次,点赞3次,收藏2次。<el-table-column prop="studentSex" label="性别" :formatter="sex"></el-table-column>然后就在vue的methods中写方法就OK了methods: { sex(row,index){ if(row.studentSex == 1){ return '男'; }else{ return '女'; }..._elementui table 性别

java文件操作之移动文件到指定的目录_java中怎么将pro.txt移动到design_mode_code根目录下-程序员宅基地

文章浏览阅读1.1k次。java文件操作之移动文件到指定的目录_java中怎么将pro.txt移动到design_mode_code根目录下

推荐文章

热门文章

相关标签