linux文件编程_进程通信

1.进程间通信介绍

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。那么释放的资源可能是其他进程需要的,然而进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

1. 无名管道通信

管道 ,通常指无名管道,是 UNIX 系统IPC最古老的形式

它是半双工的(即数据只能在一个方向上流动),具有固定读端和写端

它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

当数据从管道中被读取时,它会立即从管道中移除,无法重复读取。这种特性使得管道非常适合流式数据传输,但不适合需要保留数据的情况。

原理

管道通信的原理基于文件系统中的一个缓冲区,这个缓冲区由读进程和写进程共享。当一个进程向管道中写入数据时,内核会将数据放入这个缓冲区。如果此时没有进程在读取数据,那么数据就会一直存放在缓冲区内。反之,如果有进程在读取数据,那么内核就会将缓冲区中的数据发送给读进程。

1.1. 无名管道的创建与关闭:

无名管道是基于文件描述符的通信方式。当一个管道创建时,它会创建两个文件描述符:fd[0] 、fd[1] 。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道,如下图,这样就构成了一个单向的数据通道:

  • 管道关闭时只需要用 close() 函数将这两个文件描述符关闭即可
1.2. 创建无名管道pipe函数原型和头文件:

常用API:pipe

如何创建无名管道:在C语言中,创建无名管道可以使用**pipe()**函数。pipe函数的原型如下:

声明

#include <unistd.h>
int pipe(int pipefd[2]);

参数

  • pipefd是一个包含两个整数的数组,分别表示无名管道的两个文件描述符。
    • 第一个文件描述符pipefd[0]用于读取数据,
    • 第二个文件描述符pipefd[1]用于写入数据。

返回值

**pipe()**函数返回0表示成功,返回-1表示失败。

/*Linux下man 2 pipe查看头文件
*/ 
#include <unistd.h>
int pipe(int fd[2]);
int 		函数返回值,管道创建成功返回0,失败则返回-1
int fd[2]	包含两个元素的整型数组,存放管道对应的文件描述符,fd[0]为读而打开,fd[1]为写而打开
1.3. 无名管道应用:

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>int main()
{int fd[2];int fp;int pid;char *writeBuf = "hello from father";char readBuf[128];//int pipe(int pipefd[2]);fp = pipe(fd);                                      //创建一个管道if(fp != 0){										// 判断管道是否创建成功printf("创建管道失败\n");}pid = fork();                                       //创建一个子进程if(pid < 0){printf("创建子进程失败\n");}else if(pid > 0){sleep(3);printf("进入父进程\n");close(fd[0]);                                   //父进程关闭管道读端write(fd[1],writeBuf,strlen(writeBuf));         //父进程将writeBuf的内容写到管道里printf("父进程数据写入完毕\n");wait(NULL);                                     //父进程等待子进程退出}else{printf("进入子进程\n");close(fd[1]);                                   //子进程关闭管道写端read(fd[0],readBuf,sizeof(readBuf));            //子进程把管道中的数据读到readBuf里面printf("子进程读取父进程的数据是:%s\n",readBuf);exit(0);                                        //子进程退出}return 0;
}

运行代码:

2. 有名管道FIFO

FIFO( First Input First Output)简单说就是指先进先出,也称为命名管道,它是一种文件类型。

2.1. FIFO的特点:
  1. FIFO可以在无关的进程之间交换数据,这一点与无名管道不同。通过FIFO的文件路径名,不相关的进程都能访问同一个FIFO进行数据的读写。
  2. FIFO有路径名与之相关联,它以一种特殊的文件形式存在于文件系统中。
  3. FIFO严格遵守先进先出的原则,读总是从开始读取数据,写数据写入末尾不支持**lseek ()**文件定位操作。
  4. 管道的进程间通信是基于字节流的,且自带同步互斥机制,但只能进行单向传输。一般而言,进程退出,管道释放,所以管道的生命周期是随进
2.2. FIFO创建函数mkfifo函数原型和头文件:

如何创建有名管道:在C语言中,创建有名管道可以使用**mkfifo()**函数。mkfifo函数的原型如下:

/*Linux下 man 3 mkfifo查看手册
*/#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);int						函数返回值,成功:则返回 0	失败:返回 -1 , 错误原因存于 errno 中
const char *pathname 	创建管道的文件名/文件路径
mode_t mode				权限模式 如:0600就是可读可写模式
1.可读:        r         4
2.可写:        w         2
3.可执行        x         1
0600:6代表4+2(可读可写)
2.3. 使用mkfifo函数创建一个有名管道:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>int main()
{mkfifo("./file",0600);    //创建一个命名管道return 0;
}

注意:如果乌班图系统使用的是共享文件夹,则创建有名管道是失败的,需要换一个位置,

当使用 vi(编辑代码)、gcc(编译代码) 、运行(运行代码)时,需要前面临时加上sudo指令。

  1. ll 文件详细信息

2.3.1. 创建一个有名管道2:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>int main()
{int ret = mkfifo("./file",0600);    //创建一个命名管道if(ret == 0){                       //函数返回值为0代表创建管道成功printf("mkfifo创建管道成功\n");}if(ret == -1){                      //函数返回值为-1代表创建管道失败printf("mkfifo创建管道失败\n");  perror("why");                  //输出错误原因}return 0;
}

代码运行:

2.3.2. 创建一个有名管道3:
 
int main()
{if ((mkfifo("./file",0600) == -1) && errno == EEXIST){   //创建一个命名管道printf("mkfifo创建管道失败\n");perror("why");         }else{                       //函数返回值为0代表创建管道成功if(errno == EEXIST){printf("file you\n");}else{printf("mkfifo创建管道成功\n");}}return 0;
}
  • mkfifo("./file",0600) == -1
  • 这里表示 mkfifo 函数创建管道失败时会返回 -1,表示出错。这个条件是正确的。
  • && errno == EEXIST
  • 如果创建失败,并且 errnoEEXIST,意味着文件已经存在。这部分的逻辑是有问题的,因为在第一次调用 mkfifo 时,errno 还没有被设置到正确的值。
  • 理论上,你应该先判断 mkfifo 的返回值,只有在它返回 -1 时,才需要去检查 errno 的值。
  • else 块:
  • 如果 mkfifo 成功,返回 0,那 errno 是不确定的(不会被设置),不应该在 else 块中检查 errno

2.3.3. 创建一个有名管道3:
#include <stdio.h>
#include <errno.h>
#include <sys/stat.h>int main()
{// 尝试创建一个名为 "./file" 的命名管道,权限为 0600if ((mkfifo("./file", 0600) == -1) && errno != EEXIST) {// 如果 mkfifo 失败,并且失败的原因不是文件已存在,输出错误信息printf("mkfifo创建管道失败\n");perror("why"); // 输出详细错误信息} else {// 管道创建成功,或者文件已经存在printf("mkfifo管道已存在或创建成功\n");}return 0;
}

代码运行:-

2.4. 2.4 使用mkfifo函数创建一个管道实现两个进程之间的通信:

FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  1. 若没有指定O_NONBLOCK(默认为阻塞模式) ,只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
  2. 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
2.4.1. 总结
  • 在默认阻塞模式下,open 需要双方(读和写)都准备好才会完成,确保 FIFO 数据传输有完整的接收方和发送方。
  • 在非阻塞模式下,open 函数会立即返回,不会等待另一个进程,但在没有准备好对方进程时,写操作可能会出错,读取操作可能返回空数据。
2.4.2. 使用示例:
  • 阻塞模式打开(默认)
int fd = open("myfifo", O_RDONLY);  // 阻塞直到有进程以写模式打开 FIFO
  • 非阻塞模式打开
int fd = open("myfifo", O_RDONLY | O_NONBLOCK);  // 立即返回,不阻塞

代码编辑:

int main()
{  	int cut = 0;char but[45] = {0};if ((mkfifo("./file",0600) == -1) && errno != EEXIST){   //创建一个命名管道printf("mkfifo创建管道失败\n");perror("why");         }int fd = open("./file",O_RDONLY);printf("open success\n");	while(1){int ne_read = read(fd,but,45);cut++;printf("ne_read %d byte from fifo,context :%s \n",ne_read,but);if(cut == 5){break;}}close(fd);return 0;
}
int main()
{int cut = 0;char *str = "hello world from fifo";int fd = open("./file",O_WRONLY);printf("write open success\n");while(1){write(fd,str,strlen(str));if(cut == 5){break;}}close(fd);return 0;
}lxl@lxl-virtual-mach

先运行read,令其阻塞,再运行write,向管道写入信息。

3. 消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

3.1. 消息队列的特点:

消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

3.2. 消息队列 创建/打开函数msgget()原型和头文件:
/*Linux下 man 2 msgget查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);	//int			函数返回值,成功:返回消息队列的ID	  出错:-1,错误原因存于error中
key_t key	函数ftok的返回值(ID号)或IPC_PRIVATEint msgflg	
1. IPC_CREAT:创建新的消息队列。
2. IPC_EXCL:与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。 
3. IPC_NOWAIT:读写消息队列要求无法满足时,不阻塞。返回值: 调用成功返回队列标识符,否则返回-1./*	函数说明:用于创建一个新的或打开一个已经存在的消息队列,此消息队列与key相对应	*/   
3.3. 消息队列 发送消息函数msgsnd()原型和头文件:
/*Linux下 man 2 msgsnd查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);int				函数返回值,成功返回0,失败返回-1 错误原因存于error中
int msqid		由msgget函数返回的消息队列标识码,表示往哪个消息队列发数据void *msgp		发送给队列的消息。是⼀个指针,指针指向准备发送的消息(即准备发送的消息的内容)msgp定义的参照格式如下:struct msgbuf {long mtype;       /* message type, must be > 0 */char mtext[128];  /* message data */};long mtype		:它必须以⼀个long int⻓整数开始,接收者函数将利⽤这个⻓整数确定消息的类型char mtext[128]	:保存消息内容的数组或指针,它必须小于系统规定的上限值size_t msgsz	要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度int msgflg
1. 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
2. IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
3. IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。   /*	函数说明:将msgp消息写入到标识符为msqid的消息队列	*/    
3.4. 消息队列 接收消息函数msgrcv()原型和头文件:
/*
`Linux下 man 2 msgrcv查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);ssize_t			函数返回值,成功返回:实际读取到的消息数据长度,失败返回-1,错误原因存于error中int msqid		由msgget函数返回的消息队列标识码,表示从哪个消息队列拿数据
void *msgp		指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构 
size_t msgsz	是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型
long msgtyp		接收消息的类型,这个msgtyp和结构体msgbuf内的msgtyp是一样的int msgflg    	这个参数依然是控制函数行为的标志,取值可以是:0,表示忽略。在 msgrcv 函数中,最后一个参数 flag 控制着接收消息的行为。这个参数的值可以是 0 或者一些其他的标志,它们影响着函数的阻塞行为以及消息的接收方式。当 flag 参数为 0 时,表示接收消息的行为是阻塞的。也就是说,如果当前消息队列中没有符合条件的消息可供接收,msgrcv 函数会一直等待,直到有合适的消息到达为止。这样的阻塞模式通常用于需要等待消息到达的场景,使得进程能够在没有消息时挂起,直到有消息可用。除了 0 之外,还有其他的一些标志可以传递给 flag 参数,例如:IPC_NOWAIT: 表示非阻塞模式,如果没有符合条件的消息可供接收,函数会立即返回,并返回一个错误码(例如 -1),而不会等待消息到达。
MSG_NOERROR: 表示在接收消息时,如果消息长度超过了缓冲区的大小,则截断消息而不会产生错误。
其他一些标志,具体取决于系统的实现和特性。
因此,当 flag 参数为 0 时,表示 msgrcv 函数以阻塞模式接收消息,直到有消息到达为止。/*	函数说明:从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除	*/ 
3.5. 控制消息队列函数msgctl()原型和头文件:
/*Linux下 man 2 msgctl查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);int						函数返回值,成功返回0,失败返回-1,错误原因存于error中int msqid				由msgget函数返回的消息队列标识码int cmd					是将要采取的动作(有三个可取值)一般用IPC_RMID,这时候表示移除消息队列的意思  
1. IPC_STAT:读取消息队列的数据结构msqid_ds,并将其存储在b u f指定的地址中。
2. IPC_SET:设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。
3. IPC_RMID:从系统内核中移走消息队列。struct msqid_ds *buf	消息队列管理结构体,请参见消息队列内核结构说明部分,一般buf = NULL;/*	函数说明:获取和设置消息队列的属性	*/
3.6. 获取消息队列函数ftok()原型和头文件:
/*Linux下 man ftok查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);key_t					函数返回值,成功返回key_t值(即IPC 键值),失败返回-1const char *pathname	指定的文件名(已经存在的文件名),一般使用当前目录,比如:key_t key;key = ftok(".",1);	//这样就是将pathname设为当前目录int proj_id				子序号。虽然是int类型,但是只使用8bits(1-255)/*函数说明:
系统IPC键值的格式转换函数,系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函	数得到。
通过ftok函数生成key值,函数ftok把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值(也称IPC key键值)。
*/

代码编译:

int main()
{int msgId = msgget(0x1234, IPC_CREAT | 0777);                     //	试获取一个键为 0x1234 的消息队列。//	如果它不存在,那么 msgget 会创建一个新的消息队列,//	并设置其权限为 0777,即所有用户都有读写权限。if(msgId != 0){//	如果创建失败,就原样打印。printf("get que failuer\n");}msgrcv(msgId,);return 0;
}

管道关闭时只需要用 close() 函数将这两个文件描述符关闭即可

3.7. 使用消息队列实现两个进程之间的通信01:
struct msbuf {long mtype;       /* message type, must be > 0 */char mtext[128];    /* message data */};int main()
{struct msbuf readBuf;struct msbuf strBuf = {898,"hello world2!!"};int msgId = msgget(0x1234, IPC_CREAT|0777);if(msgId == -1){printf("get que failuer\n");}msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);printf("read from que %s\n",readBuf.mtext);msgsnd(msgId,&strBuf,strlen(strBuf.mtext),0);return 0;
}
struct msgbuf {long mtype;       /* message type, must be > 0 */char mtext[128];    /* message data */};int main()
{struct msgbuf readBuf = {888,"hello world!!!"};struct msgbuf readBuf2;int msgId = msgget(0x1234, IPC_CREAT|0777);if(msgId == -1){printf("get que failuer\n");}msgsnd(msgId,&readBuf,strlen(readBuf.mtext),0);msgrcv(msgId,&readBuf2,sizeof(readBuf2.mtext),898,0);printf("read from que %s\n",readBuf2.mtext);return 0;
}

先使用发送端,发送888,"hello world!!!"这个内容,接收端接收888,"hello world!!!"这个内容,之后接收端发送898,"hello world2!!"这个内容,发送端接收898,"hello world2!!"这个内容。

3.7.1. 使用消息队列实现两个进程之间的通信02:

ftok 获取key

#include <stdio.h>            // 标准输入输出库
#include <sys/types.h>        // 定义数据类型的库
#include <sys/ipc.h>          // IPC 相关函数的库
#include <sys/msg.h>          // 消息队列的库
#include <string.h>           // 字符串操作库
mkskmsmmxxn  .cq9
// 自定义消息结构体 msbuf,包含消息类型和消息内容
struct msbuf {long mtype;       /* 消息类型,必须大于 0 */char mtext[128];  /* 消息数据 */
};int main()
{// 声明读取和发送消息的缓冲区struct msbuf readBuf;                  // 用于接收消息的缓冲区struct msbuf strBuf = {898,"hello world2!!"};  // 准备发送的消息,类型为 898// 使用 ftok 函数生成一个唯一的 key,用于标识消息队列key_t key;key = ftok(".",'m');  // '.' 是当前目录,'m' 是项目标识符printf("key=%x\n",key);  // 打印生成的 key 值// 创建或获取消息队列,权限设置为 0777(可读写执行)int msgId = msgget(key, IPC_CREAT|0777);// 如果创建失败,打印错误消息if(msgId == -1){printf("get que failure\n");}// 从消息队列中接收类型为 888 的消息,并存储在 readBuf 中msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);printf("read from que %s\n", readBuf.mtext);  // 打印从消息队列中读取的消息// 将 strBuf 中的消息发送到消息队列,发送消息的长度为 strBuf.mtext 的长度msgsnd(msgId, &strBuf, strlen(strBuf.mtext), 0);// 删除消息队列,释放资源msgctl(msgId, IPC_RMID, NULL);return 0;
}
#include <stdio.h>            // 标准输入输出库
#include <sys/types.h>        // 定义数据类型的库
#include <sys/ipc.h>          // IPC 相关函数的库
#include <sys/msg.h>          // 消息队列的库
#include <string.h>           // 字符串操作库// 定义一个结构体 msgbuf 用于消息传递,包含消息类型和消息内容
struct msgbuf {long mtype;        // 消息类型,必须大于 0char mtext[128];   // 消息内容,最大长度为 128 字节
};int main()    // 主函数
{// 初始化一个消息结构体 readBuf,类型为 888,消息内容为 "hello world!!!"struct msgbuf readBuf = {888, "hello world!!!"}; struct msgbuf readBuf2;  // 声明另一个结构体用于接收消息// 生成消息队列的唯一 key,使用当前目录 (".") 和字符 'm'key_t key;key = ftok(".", 'm');  // 生成一个唯一的键值,用于标识消息队列printf("key=%x\n", key);  // 输出生成的键值// 创建或获取消息队列,权限为 0777,表示所有用户可以读写执行int msgId = msgget(key, IPC_CREAT | 0777);if (msgId == -1) {  // 如果返回值为 -1,表示获取消息队列失败printf("get que failure\n");}// 向消息队列发送消息,类型为 888,内容为 "hello world!!!"msgsnd(msgId, &readBuf, strlen(readBuf.mtext), 0);// 从消息队列接收消息,接收类型为 898 的消息,并存储在 readBuf2 中msgrcv(msgId, &readBuf2, sizeof(readBuf2.mtext), 898, 0);printf("read from que %s\n", readBuf2.mtext);  // 输出接收到的消息内容// 删除消息队列,释放 IPC 资源msgctl(msgId, IPC_RMID, NULL);return 0;  // 程序正常结束
}

代码使用

我们可以看到两个进程可以在同一个消息队列中进行通信。

注意:

struct msgbuf 已经在 <sys/msg.h> 中定义,且本代码中不能再次重新定义它。需要使用其他函数定义。

4. 共享内存

共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。

4.1. 共享内存的特点:
  • 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  • 因为多个进程可以同时操作,所以需要进行同步。
  • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问(共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。)
  1. 原型函数:
#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag),
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
4.2. 共享内存创建/获取函数shmget()原型和头文件:
/*Linux下 man 2 shmget查看手册
*/
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);int				函数返回值,成功返回共享内存的标识符ID,失败则返回-1key_t key		通常要求此值来源于ftok返回的IPC键值
1. 0(IPC_PRIVATE):会建立新共享内存对象
2. 大于0的32位整数:视参数shmilg来确定操作。    size_t size		共享内存的大小		
1. 大于0的整数:新建的共享内存大小,以字节为单位
2. 0:只获取共享内存时指定为0int shmflg		权限标志,常用两个IPC_CREAT和IPC_EXCL,一般后面还加一个权限,相当于文件的权限 
1. IPC_CREAT:创建一个共享内存返回,已存在打开返回
2. IPC_EXCL:配合着IPC_CREAT使用,共享内存已存在出错返回。
一般使用:IPC_CREAT | IPC_EXCL | 0666    /*函数说明:得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符*/
4.3. 共享内存映射函数shmat()原型和头文件:
/*Linux下 man 2 shmat查看手册
*/
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);void *					函数返回值,成功返回指向共享内存的地址,失败返回-1,错误原因在erron中int shmid				共享内存标识符,shmget的返回值
const void *shmaddr		指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
int shmflg				SHM_RDONLY:为只读模式,其他为读写模式,设为0系统默认   /*函数说明:
shmat(把共享内存区对象映射到调用进程的地址空间),连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问
*/
4.4. 断开与共享内存的连接函数shmdt()原型和头文件:
/*Linux下 man 2 shmdt查看手册
*/
#include <sys/types.h>
#include <sys/shm.h>int shmdt(const void *shmaddr);int						函数返回值,成功返回0,失败返回-1
const void *shmaddr		连接的共享内存的起始地址,shmat的返回值  /*函数说明:
断开与共享内存的连接,与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存
*/   
4.5. 共享内存控制/删除函数shmctl()原型和头文件:
/*Linux下 man 2 shmctl查看手册
*/
#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);int						函数返回值,成功返回0,失败返回-1
int shmid				共享内存标识符int cmd					共享内存控制的方式
1. IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
2. IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
3. IPC_RMID:删除这片共享内存struct shmid_ds *buf	 共享内存管理结构体,具体说明参见共享内存内核结构定义部分。删除共享内存的时候,一般设置为NULL/*函数说明:完成对共享内存的控制*/ 
4.6. 使用共享内存实现两个进程之间的通信:
  • 创建共享文件,并放入数据
#include <sys/ipc.h>    // 包含IPC(进程间通信)相关的头文件,用于共享内存、信号量等
#include <sys/shm.h>    // 包含共享内存相关的头文件
#include <stdlib.h>     // 包含标准库函数,如exit()
#include <stdio.h>      // 包含输入输出函数,如printf()
#include <string.h>     // 包含字符串操作函数,如strcpy()
#include <unistd.h>     // 包含系统调用函数,如sleep()int main()
{
//	int shmget(key_t key,size_t size,int shmflg);int shmid;char *shmaddr;int z = 1;key_t key ;key = ftok(".",z);shmid = shmget(key,1024*4,0);if(shmid == -1){printf("创建共享内存失败\n");exit(-1);}shmaddr = shmat(shmid,NULL,0);printf("shmat ok!\n");printf("data :%s\n",shmaddr);shmdt(shmaddr);printf("quit\n");return 0;
}
  • 读取共享文件中的数据,把输出
#include <sys/ipc.h>    // 包含IPC(进程间通信)相关的头文件,用于共享内存、信号量等
#include <sys/shm.h>    // 包含共享内存相关的头文件
#include <stdlib.h>     // 包含标准库函数,如exit()
#include <stdio.h>      // 包含输入输出函数,如printf()
#include <string.h>     // 包含字符串操作函数,如strcpy()
#include <unistd.h>     // 包含系统调用函数,如sleep()int main() 
{
//    int shmget(key_t key,size_t size,int shmflg);  // 函数声明,shmget用于获取共享内存段int shmid;        // 定义共享内存段的ID变量char *shmaddr;    // 定义指向共享内存段的指针int z = 1;        // 定义ftok函数使用的proj_id参数,作为键值生成的一部分key_t key;        // 定义共享内存的键key = ftok(".", z);  // 使用ftok生成唯一的键值,"."表示当前目录,z是proj_id的整数值shmid = shmget(key, 1024 * 4, IPC_CREAT | 0667);  // 分配一个4KB大小的共享内存段,并赋予权限0667if (shmid == -1) {    // 检查shmget是否成功,如果失败则输出错误信息printf("创建共享内存失败\n");exit(-1);         // 程序异常退出}shmaddr = shmat(shmid,NULL,0);  // 将共享内存段附加到当前进程的地址空间printf("shmat ok!\n");         // 提示共享内存附加成功strcpy(shmaddr, "hello world"); // 将字符串"hello world"复制到共享内存段sleep(5);    // 暂停程序5秒,确保有时间查看共享内存的内容shmdt(shmaddr);         // 分离共享内存段shmctl(shmid, IPC_RMID, 0); // 删除共享内存段printf("quit\n");  // 程序结束,输出提示return 0;          // 正常退出程序
}
  • 代码实现:

4.7. 用指令来查看和释放已经存在的共享内存:
  • 查看共享文件:
ipcs -m				//查看系统中的共享内存段

  • 释放已经存在的共享内存
ipcrm -m shmid		//释放系统中的已有共享内存段(shmget返回值)

5. 信号

对于Linux来说,实际信号是软中断,许多重要的程序都需要处理信号,信号为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。

5.1. 信号的名称和编号:
  1. 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
  2. 信号定义在signal.h头文件中,信号名都定义为正整数。
  3. 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号,kill对于信号0又特殊的应用。

5.2. 信号的处理:

信号的处理有三种方法,分别是:忽略、捕捉和默认动作。

  • 忽略信号: 进程可以选择忽略大多数信号,这意味着当信号到达时,进程不会对它做出反应,继续运行。
    但有两个信号不能被忽略,分别是:
    • SIGKILL:它立即终止进程,无法捕获或忽略。它通常用于强制杀死某个进程,因为即使进程进入了无响应状态,发送 SIGKILL 仍然能终止它。
    • SIGSTOP:这个信号会使进程暂停(停止执行),也无法捕获或忽略。类似地,这个信号可以用于调试或暂时挂起某个进程。

这些信号不可忽略是为了防止进程无法控制或挂起,保持系统的可管理性。

  • 捕捉信号: 捕捉信号是指,程序员可以为特定信号定义自定义的处理方式。例如,当程序收到某个信号时,执行自定义的函数而不是使用系统默认的行为。这种机制通常用于执行一些清理工作,比如程序即将退出时保存文件、释放资源等。你需要:
  • 定义一个信号处理函数(signal handler)。
  • 使用 signal() 函数或 sigaction() 告诉内核,当该信号到达时,调用你定义的函数。

例如:

void signal_handler(int signum) {printf("Received signal %d\n", signum);// 执行特定的处理逻辑
}int main() {signal(SIGINT, signal_handler);  // 捕捉 Ctrl+C(SIGINT)while (1) {printf("Running...\n");sleep(1);}
}
    • 当程序运行并按下 Ctrl+C 时,它会捕捉到 SIGINT 信号并调用 signal_handler() 函数,而不是立即终止程序。
    • (说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。)
  • 系统默认动作: 每个信号都有一个系统默认的处理动作。如果程序不捕捉或忽略信号,系统会按照默认动作执行。大多数信号的默认处理方式是终止程序(例如 SIGTERM 和 SIGINT),有些信号的默认行为是忽略(如 SIGCHLD)。
    这些默认动作为确保进程在某些情况下能迅速响应信号,避免程序进入死循环或僵尸进程状态。

如果没有特别处理,系统会按照这些默认行为进行反应。,

系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。

具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。

5.3. 信号的应用之杀死进程:

其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。比如,在后台运行了一个进程,通过 ps 命令可以查看这个进程的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了该进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。

#include <stdio.h>int main()
{while(1);return 0;
}
  • 使用ps -aux|prep +程序名,查看进程ID。

  • 使用kill-9 程序id ||kill -SIGKILL 程序id 杀死进程

由于这个程序没有停止条件,kill 程序会一直运行,除了用ctrl+c和ctrl+z两个命令来结束该进程外,还有用kill -9 PID来结束,先用指令-aux|grep kill查看kill进kill的PID是41955,输入kill -9 5792或者kill SIGKILL 5806都可以杀死进程,对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段。(注:进程id是随机生成的)

5.4. 信号处理函数的注册和信号处理发送函数:
  • 信号处理函数的注册不只一种方法,分为入门版和高级版
  1. 入门版:signal
  2. 高级版:sigaction
  • 信号发送函数也不止一个,同样分为入门版和高级版
  1. 入门版:kill
  2. 高级版:sigqueue
5.5. 信号注册函数——入门版
5.5.1. 信号注册函数signal()原型和头文件:
/*Linux下 man 2 signal查看手册
*/
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);sighandler_t			函数返回值,返回信号处理程序的前一个值,或者在错误时SIG ERR。如果发生错误,则设置errno来指示原因。int signum				指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号sighandler_t handler	描述了与信号关联的动作,它可以取以下三种值:  1. 一个无返回值的函数地址
此函数必须在signal()被调用前声明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:
void handler(int signum);2. SIG_IGN
这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。3. SIG_DFL
这个符号表示恢复系统对信号的默认处理。

根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。 对于sighandler_t signal(int signum, sighandler_t handler);函数来说,signum 显然是信号的编号,handler 是中断函数的指针。 同样,typedef void (*sighandler_t)(int);中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。

5.5.2. 捕捉信号1:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>void signal_handler(int signum) {printf("\nReceived signal %d\n", signum);// 执行特定的处理逻辑
}int main() {signal(SIGINT, signal_handler);  // 捕捉 Ctrl+C(SIGINT)while (1) {printf("Running...\n");sleep(1);}
}

运行代码:

使用ctrl + c 无法结束进程 可以捕捉到对进程操作的信号!

0

ctrl+c 无法结束进程,可以使用kill -9 进程id 杀死进程

5.5.3. 捕捉信号2:

代码设计:

#include <signal.h>  // 包含处理信号的库
#include <stdio.h>   // 标准输入输出库
#include <unistd.h>  // 提供sleep等函数// 自定义的信号处理函数,当捕捉到信号时会调用这个函数
void signal_handler(int signum) {printf("\nReceived signal %d\n", signum);// 执行特定的处理逻辑// 根据捕捉到的信号进行不同的处理switch(signum){case 2:printf("ctrl +v\n");break;case 9:printf("sigkill\n");break;case 10:printf("sigint\n");break;}
}int main() {signal(SIGINT, signal_handler);  // 捕捉 Ctrl+C(SIGINT)signal(SIGKILL, signal_handler);  //捕捉杀死进程信号signal(SIGUSR1, signal_handler);   //捕捉用户自定义的信号// 无限循环,让程序一直运行以便测试信号捕捉while (1) {sleep(1);  // 暂停 1 秒,避免 CPU 过度占用}return 0;} 

代码实现:

当使用ctrl +v 以及kill -10 6799(进程id)、kill -2 6799,只会打印其对应的原样输出,当使用命令 kill -9 6799 仍然会杀死进程。

5.6. 忽略信号SIG_IGN
#include <stdio.h>
#include <signal.h>void handler(int signum)
{printf("get signum = %d\n",signum);switch(signum){case 2:printf("SIGINT\n");break;case 9:printf("SIGKILL\n");break;case 10:printf("SIGUSR1\n");break;}printf("never quit\n");
}int main()
{//typedef void (*sighandler_t)(int);//sighandler_t signal(int signum, sighandler_t handler);signal(SIGINT,SIG_IGN);     //忽略Ctrl + C信号signal(SIGKILL,SIG_IGN);    //忽略杀死进程信号signal(SIGUSR1,SIG_IGN);    //忽略用户自定义的信号1while(1);return 0;
}
  • 代码实现

  • 对 Ctrl+C/SIGINT 和 用户自定义信号1/SIGUSR1起忽略作用,但对 SIGKILL 不可忽略。
5.7. 信号发送函数——入门版
5.7.1. 信号发送函数kill()原型和头文件:
/*Linux下 man 2 kill查看手册
*/
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);int 			函数返回值,如果成功(至少发了一个信号)则返回0,失败则返回-1pid_t pid		指定接收进程的进程ID
int sig			信号编号或者是信号的类型,可以通过kill -l指令查看
5.7.2. 使用信号发送函数kill()实现信号的发送:
#include <signal.h>  // 提供信号相关的函数,如 kill()
#include <stdio.h>   // 标准输入输出库
#include <unistd.h>  // 提供系统调用和各种系统级功能,如 sleep()
#include <stdlib.h>  // 提供标准库函数,如 atoi() 用于字符串到整数的转换int main(int argc , char **argv) {int signum;  // 存储从命令行输入的信号编号int pid;     // 存储从命令行输入的进程ID (PID)// 将命令行参数 argv[1] 转换为整数,并存储到 signum 中,用于表示信号编号signum = atoi(argv[1]);// 将命令行参数 argv[2] 转换为整数,并存储到 pid 中,用于表示目标进程的 PIDpid = atoi(argv[2]);// 输出信号编号和进程ID,方便调试和确认printf("num = %d ,pid = %d\n", signum, pid);// 使用 kill() 系统调用,发送指定的信号 (signum) 给目标进程 (pid)kill(pid, signum);// 打印确认信息,表示信号已成功发送printf("发送信号成功!\n");return 0;  // 返回 0 表示程序正常结束
}

atoi() 函数是 C 标准库中的一个函数,用于将字符串转换为整数。

5.7.3. 使用sprintf和system完成与kill一样的功能:
#include <stdio.h>   // 提供标准输入输出函数,如 printf(), sprintf()
#include <stdlib.h>  // 提供标准库函数,如 atoi(), system()int main(int argc, char **argv) {int signum;  // 用于存储信号编号int pid;     // 用于存储进程 IDchar cmd[128] = {0};  // 用于存储构建的系统命令字符串,128字节足够大以保存命令// 将命令行参数转换为整数signum = atoi(argv[1]);  // 将字符串形式的信号编号转换为整数pid = atoi(argv[2]);     // 将字符串形式的进程 ID 转换为整数printf("num = %d ,pid = %d\n", signum, pid); // 打印信号编号和进程 ID// 使用 sprintf 构建 shell 命令,将信号编号和进程 ID 组合成一个字符串sprintf(cmd, "kill -%d %d", signum, pid);// 使用 system() 函数执行命令,将构建的 kill 命令发送到系统system(cmd);return 0;  // 返回 0 表示程序正常结束
}

5.8. 信号注册函数——高级版

为什么会有高级版,我们的入门版虽然可以发出和接收到了信号,但我们想发出信号的同时携带点数据,这时候需要用到高级版 sigaction。

5.8.1. 信号接收函数sigaction()原型和头文件:
/*Linux下 man 2 sigaction查看手册
*/
#include <signal.h>int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);const struct sigaction {void     (*sa_handler)(int);//信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作void     (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,//把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。int      sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};
//回调函数句柄sa_handler、sa_sigaction只能任选其一
  • sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中:
  1. 第一个参数signum:是注册的信号的编号,可以使用kill -1指令查看
  2. 第二个参数act:如果不为空说明需要对该信号有新的配置;
  3. 第三个参数oldact:如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

===================================================================

  • 第二个参数act--结构体struct sigaction说明:

(1)void (*sa_handler)(int); 不携带数据,作用与入门版类似。

(2)void (*sa_sigaction)(int, siginfo_t *, void *); 处理函数来说还需要有一些说明。

  • int 是接收注册信息的编号;
  • void * 是接收到信号所携带的额外数据;
  • struct siginfo 这个结构体主要适用于记录接收信号的一些相关信息;

注意:sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

(3)sa_mask sigset_t sa_mask是一个信号集,在调用该信号捕捉函数之前,将需要block的信号加入这个sa_mask,仅当信号捕捉函数正在执行时,才阻塞sa_mask中的信号,当从信号捕捉函数返回时进程的信号屏蔽字复位为原先值;因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。

(4)sa_flags 是一个选项,注意:这个选项只与sigaction函数注册的信号有关联,与sa_mask中的信号无任何关系

  • SA_INTERRUPT 由此信号中断的系统调用不会自动重启
  • SA_RESTART 由此信号中断的系统调用会自动重启
  • SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针
  • SA_NODEFER 一般情况下,当信号处理函数运行时,内核将阻塞(sigaction函数注册时的信号)。但是如果设置了SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。
  • SA_RESETHAND 当调用信号处理函数时或信号处理函数结束后,将信号的处理设置为系统默认值。

===================================================================

  • struct siginfo结构体说明:

void (*sa_sigaction)(int signum,siginfo_t *info,void *ucontext);

其中void *是接收到信号所携带的额外数据;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息。


typedef struct {int si_signo;           // 信号编号int si_errno;           // 保存错误代码int si_code;            // 信号代码int si_trapno;          // 陷阱号pid_t si_pid;           // 发送信号的进程IDuid_t si_uid;           // 发送信号的用户IDint si_status;          // 退出状态或信号clock_t si_utime;       // 用户态运行时间clock_t si_stime;       // 内核态运行时间sigval_t si_value;      // 信号附加值int si_int;             // 附加整数值void *si_ptr;           // 附加指针值int si_overrun;         // 定时器超时次数int si_timerid;         // 定时器IDvoid *si_addr;          // 导致信号的内存地址long si_band;           // 产生信号的事件int si_fd;              // 文件描述符short si_addr_lsb;      // 地址最低有效位void *si_lower;         // 低地址void *si_upper;         // 高地址int si_pkey;            // 保护密钥siginfo_t *si_call_addr; // 调用地址int si_syscall;         // 系统调用号unsigned int si_arch;   // 硬件特定的错误代码
} 

关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时在si_int或者si_ptr成员中也保存了对应的数据。

5.9. 信号发送函数——高级版
5.9.1. 信号发送函数sigqueue()原型和头文件:
/*Linux下 man 3 sigqueue查看手册
*/
#include <signal.h>int sigqueue(pid_t pid, int sig, const union sigval value);int 				函数返回值,如果sigqueue()返回0,表示信号已成功排队到接收进程。否则,返回-1,并设置errno来表示错误。pid_t pid			目标进程的进程ID
int sig				信号编号或者是信号的类型,可以通过kill -l指令查看    union sigval value	一个联合体,表示信号附带的数据,附带数据可以是一个整数也可以是一个指针,有如下形式:union sigval {int   sival_int;void *sival_ptr;
};
5.10. 使用信号接收函数sigaction()和信号发送函数sigqueue()实现两个进程的通信:
  • sigaction()接收函数
/*NiceSignal.c*/
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>void handler(int signum, siginfo_t *info, void *context)    //信号处理函数
{printf("signum = %d\n",signum);  //打印kill操作值if(context != NULL){printf("获取到的PID = %d\n",info->si_pid);    //使用siginf_t结构体中的si_pid,获取发送者的PIDprintf("获取到的数据是:%d\n",info->si_int);	 //使用siginf_t结构体中的si_int,用来存储附加的信号信息。printf("获取到的数据是:%d\n",info->si_value.sival_int);	//使用siginf_t结构体中的si_value.sival_int,sigqueue 发送信号时附带的值}
}int main()
{struct sigaction act; //act需要干什么的结构体	printf("my pid = %d\n",getpid());  // 打印自身进程的PIDact.sa_sigaction = handler;//将信号处理程序设置为 handler 函数。这意味着,当接收到信号时,系统会调用 handler。act.sa_flags = SA_SIGINFO;      //将标志位指定为此,才可以接收数据//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);sigaction(SIGINT,&act,NULL);  // SIGINT	CTRL+c    act需要干什么	  NULL(备份)默认为空while(1);return 0;
}
/*send.c*/
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char **argv)
{if(argc != 3){printf("请重新输出3个参数\n");exit(-1);}int pid;int signum;union sigval value;pid = atoi(argv[1]);signum = atoi(argv[2]);value.sival_int = 100;// sival_int:用于传递一个整数类型的附加数据。// sival_ptr:用于传递一个指针类型的附加数据。//int sigqueue(pid_t pid, int sig, const union sigval value);int ret = sigqueue(pid,signum,value);if(ret == -1){printf("sigqueue发送信号失败\n");}printf("PID = %d\n",getpid());printf("done\n");return 0;
}

使用信号发送函数sigqueue()将SIGINT信号发送到10106所对应的进程中了,发送的内容是100,信号接收端能够接收到来自10109进程发送的信号和内容,并且能够得到发送进程的进程ID,从而实现了两个进程间用信号的通信

6. 信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。Linux下的信号量函数都是在通用的信号量数组上进行操作,而不是 一个单一的二值信号量上进程操作,二值信号量:信号量只能取0或者1的变量

6.1. 信号量相关定义:
  • 临界资源:能被多个进程共享,但一次只能允许一个进程使用的资源称为临界资源。
  • 临界区:涉及到临界资源的部分代码,称为临界区。
  • 互斥:亦称间接制约关系,在一个进程的访问周期内,另一个进程就不能进行访问,必须进行等待。当占用临界资源的进程退出临界区后,另一个进程才允许去访问此临界资源。
  • 例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。
  • 同步:亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的同步就是源于它们之间的相互合作。所谓同步其实就是两个进程间的制约关系。
  • 例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。
  • 原子性:对于进程的访问,只有两种状态,要么访问完了,要么不访问。当一个进程在访问某种资源的时候,即便该进程切出去,另一个进程也不能进行访问。
6.2. 信号量特点:
  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  2. 信号量基于操作系统的 PV 操作,P(拿锁)V (放回锁)程序对信号量的操作都是原子操作。
  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  4. 支持信号量组
6.3. 创建/获取一个信号量组函数semget()原型和头文件:
/*Linux下 man 2 semget查看手册
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semget(key_t key, int nsems, int semflg);int 		函数返回值,成功返回信号量标识符,再信号量的其他函数中都会使用该值,出错则返回-1key_t key	信号量的键值,多个进程可以通过它访问同一个信号量(通过ftok获取)
int nsems	信号量集中信号量的个数int semflg	标识函数的行为及权限。取值如下:    
1. IPC_CREAT:如果不存在就创建
2. IPC_EXCL和IPC_CREAT搭配使用,如果已经存在,则返回失败/*运用实例*/
semid = semget(key,1,IPC_CREAT|0666);	//获取或创建信号量
6.4. 控制信号量函数semctl()原型和头文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <stdlib.h>union semun	{int val;struct senid_ds *buf;unsigned short *array;struct seminfo *_buf;
};void pGetkey(int semid)
{ 	struct  sembuf set;set.sem_num = 0;        //信号量的编号 默认为0set.sem_op = -1;       //P操作,等待信号变得可用( -1 就是拿钥匙)set.sem_flg = SEM_UNDO;  //当进程结束时,自动取消对锁的操作。// int semop(int semid, struct sembuf *sops, size_t nsops);//semid 信号量idsemop(semid, &set, 1);   //1 只有一个信号量printf("get key\n");
}
void  vPutBackKey(int semid)
{ 	struct  sembuf set;set.sem_num = 0;        //信号量的编号 默认为0set.sem_op = 1;       //v操作,等待信号变得可用( 1 就是还钥匙)set.sem_flg = SEM_UNDO;  //当进程结束时,自动取消对锁的操作。// int semop(int semid, struct sembuf *sops, size_t nsops);//semid 信号量idsemop(semid, &set, 1);   //1 只有一个信号量printf("put back the key\n");
}
int main(int argc,char const *argv[])
{int semid;key_t key;key = ftok(".",2);  //生成有个共享文件 //	int semget(key_t key, int nsems, int semflg);// 1 表示信号量集合中有一个信号量     semid = semget(key,1,IPC_CREAT|0666);   //获取||创建信号量集合union semun initsem;initsem.val = 1;  //设置了一把锁//   int semctl(int semid, int semnum, int cmd, ...);// 0 操作第一个信号量semctl(semid,0,SETVAL,initsem);//初始化信号量//	SETVAL 设置信号量的值,设置为inisemint pid = fork();if(pid > 0 ){pGetkey(semid);//拿锁printf("this is 父进程\n");vPutBackKey(semid);// 锁放回去semctl(semid,0,IPC_RMID);               //删除信号量}else if(pid == 0) {printf("this is 子进程\n");vPutBackKey(semid);// 锁放回去}else {printf("fork error\n");exit(-1);}return 0;
}

6.5. 使用信号量来控制共享内存读写顺序问题:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <string.h>union semun     {int val;struct senid_ds *buf;unsigned short *array;struct seminfo *_buf;
};void pGetkey(int semid)
{struct  sembuf set;set.sem_num = 0;        //信号量的编号 默认为0set.sem_op = -1;       //P操作,等待信号变得可用( -1 就是拿钥匙)set.sem_flg = SEM_UNDO;  //当进程结束时,自动取消对锁的操作。// int semop(int semid, struct sembuf *sops, size_t nsops);//semid 信号量idsemop(semid, &set, 1);   //1 只有一个信号量printf("get key\n");
}
void  vPutBackKey(int semid)
{struct  sembuf set;set.sem_num = 0;        //信号量的编号 默认为0set.sem_op = 1;       //v操作,等待信号变得可用( 1 就是还钥匙)set.sem_flg = SEM_UNDO;  //当进程结束时,自动取消对锁的操作。// int semop(int semid, struct sembuf *sops, size_t nsops);//semid 信号量idsemop(semid, &set, 1);   //1 只有一个信号量printf("put back the key\n");
}
int main()
{int semid;int shmid;key_t semkey;key_t shmkey; char *shmaddr = NULL;    // 定义指向共享内存段的指针char *str = "hell:!!918 hellod !";int z = 5;        // 定义ftok函数使用的proj_id参数,作为键值生成的一部分// 定义共享内存的键semkey = ftok(".",2);  //生成有个信号量值 if(semkey == -1){printf("创建信号量IPC键值失败\n");}shmkey = ftok(".", z);  // 使用ftok生成唯一的键值,"."表示当前目录,z是proj_id的整数值if(shmkey == -1){printf("创建共享内存IPC键值失败\n");}printf("共享内存IPC键值key = 0x%x\n",shmkey);printf("信号量IPC键值key = 0x%x\n",semkey);//  int semget(key_t key, int nsems, int semflg);// 1 表示信号量集合中有一个信号量     semid = semget(semkey,1,IPC_CREAT|0666);   //获取||创建if (semid == -1) {    // 检查shmget是否成功,如果失败则输出错误信息printf("创建信号量集合失败\n");exit(-1);         // 程序异常退出}shmid = shmget(shmkey, 1024 * 4, 0);  // 分配一个4KB大小的共享内存段,并赋予权限0666if (shmid == -1) {    // 检查shmget是否成功,如果失败则输出错误信息printf("创建共享内存失败\n");exit(-1);         // 程序异常退出}union semun initsem;initsem.val = 0;  //设置了0把锁//   int semctl(int semid, int semnum, int cmd, ...);// 0 操作第一个信号量if (semctl(semid, 0, SETVAL, initsem) == -1) {//初始化信号量//  SETVAL 设置信号量的值,设置为inisemperror("semctl SETVAL failed");exit(EXIT_FAILURE);
}            shmaddr = shmat(shmid, NULL, 0);  // 将共享内存段附加到当前进程的地址空间printf("shmat ok!\n");         // 提示共享内存附加成功strcpy(shmaddr, str); // 将字符串"hello world"复制到共享内存段vPutBackKey(semid); shmdt(shmaddr); // 分离共享内存段semctl(semid,0,IPC_RMID);  printf("quit\n");  // 程序结束,输出提示return 0;          // 正常退出程序
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <string.h>// 定义一个联合体,用于信号量控制操作
union semun {int val;                   // 用于 SETVAL 命令struct senid_ds *buf;       // 用于 IPC_STAT 和 IPC_SETunsigned short *array;      // 用于 GETALL 和 SETALLstruct seminfo *_buf;       // 用于 IPC_INFO
};// 定义 P 操作,用于获取信号量(等待资源)
void pGetkey(int semid) {struct sembuf set;          // 定义一个信号量操作结构体set.sem_num = 0;            // 信号量编号,默认为0set.sem_op = -1;            // P操作,等待信号量可用(-1表示减去信号量,获取资源)set.sem_flg = SEM_UNDO;     // 自动撤销信号量操作,当进程结束时,自动释放资源// 执行信号量操作,等待信号量的可用性semop(semid, &set, 1);      // semid 是信号量id,操作1个信号量printf("get key\n");        // 输出获取信号量提示
}// 定义 V 操作,用于释放信号量(归还资源)
void vPutBackKey(int semid) {struct sembuf set;          // 定义一个信号量操作结构体set.sem_num = 0;            // 信号量编号,默认为0set.sem_op = 1;             // V操作,释放信号量(+1表示归还资源)set.sem_flg = SEM_UNDO;     // 自动撤销信号量操作,当进程结束时,自动释放资源// 执行信号量操作,释放信号量资源semop(semid, &set, 1);      // semid 是信号量id,操作1个信号量printf("put back the key\n");  // 输出释放信号量提示
}int main() {int semid;                  // 信号量IDint shmid;                  // 共享内存IDkey_t semkey;               // 信号量的键值key_t shmkey;               // 共享内存的键值char *shmaddr = NULL;       // 指向共享内存的指针char *str = "hell:!!918 hellod !";  // 要写入共享内存的字符串int z = 5;                  // 定义ftok函数使用的proj_id参数,生成键值的一部分// 生成信号量的键值semkey = ftok(".", 2);      // "." 表示当前目录,2 是项目IDif (semkey == -1) {printf("创建信号量IPC键值失败\n");}// 生成共享内存的键值shmkey = ftok(".", z);      // "." 表示当前目录,z 是项目IDif (shmkey == -1) {printf("创建共享内存IPC键值失败\n");}printf("共享内存IPC键值key = 0x%x\n", shmkey);  // 输出共享内存键值printf("信号量IPC键值key = 0x%x\n", semkey);    // 输出信号量键值// 创建或获取一个信号量集合,包含一个信号量semid = semget(semkey, 1, IPC_CREAT | 0666);  // 信号量权限为0666if (semid == -1) {printf("创建信号量集合失败\n");exit(-1);  // 程序异常退出}// 创建共享内存段,大小为 4KB,并赋予权限 0666shmid = shmget(shmkey, 1024 * 4, 0);  // 不创建新的内存段,权限为0666if (shmid == -1) {printf("创建共享内存失败\n");exit(-1);  // 程序异常退出}// 初始化信号量为0,表示加锁(资源不可用)union semun initsem;initsem.val = 0;  // 信号量初始值设为0if (semctl(semid, 0, SETVAL, initsem) == -1) {perror("semctl SETVAL failed");  // 如果信号量初始化失败,输出错误信息exit(EXIT_FAILURE);  // 程序退出}// 将共享内存段附加到当前进程的地址空间shmaddr = shmat(shmid, NULL, 0);  // 返回指向共享内存的指针if (shmaddr == (char *)-1) {perror("shmat failed");  // 检查共享内存附加是否成功exit(EXIT_FAILURE);  // 附加失败时,程序退出}printf("shmat ok!\n");  // 提示共享内存附加成功// 将字符串复制到共享内存段strcpy(shmaddr, str);  // 将 str 复制到共享内存vPutBackKey(semid);    // 释放信号量,表示操作结束// 分离共享内存段shmdt(shmaddr);  // 断开当前进程与共享内存的连接// 删除信号量集合semctl(semid, 0, IPC_RMID);  // 删除信号量printf("quit\n");  // 输出提示,程序结束return 0;  // 正常退出程序
}

注意:发送端函数,结束时只能删除信号量,不能删除共享内存段,否则就会出现段错误

#include <stdio.h>          // 包含标准输入输出库
#include <sys/types.h>      // 包含系统数据类型定义
#include <sys/ipc.h>        // 包含IPC(进程间通信)的定义
#include <sys/shm.h>        // 包含共享内存的定义
#include <sys/sem.h>        // 包含信号量的定义
#include <stdlib.h>         // 包含标准库函数,如exit()// 定义信号量控制用的联合体
union semun {int val;                // 信号量的初始值struct senid_ds *buf;   // 用于IPC_STAT和IPC_SET的缓冲区unsigned short *array;  // 用于GETALL和SETALL的数组struct seminfo *_buf;   // 用于IPC_INFO的缓冲区
};// P操作:获取信号量("拿钥匙")
void pGetkey(int semid)
{struct sembuf set;      set.sem_num = 0;        // 信号量的编号,通常为0set.sem_op = -1;        // P操作:减少信号量值("拿钥匙")set.sem_flg = SEM_UNDO; // 进程结束时,自动撤销对信号量的操作semop(semid, &set, 1);  // 执行P操作,等待信号量可用printf("get key\n");
}// V操作:释放信号量("还钥匙")
void vPutBackKey(int semid)
{struct sembuf set;set.sem_num = 0;        // 信号量的编号,通常为0set.sem_op = 1;         // V操作:增加信号量值("还钥匙")set.sem_flg = SEM_UNDO; // 进程结束时,自动撤销对信号量的操作semop(semid, &set, 1);  // 执行V操作,释放信号量printf("put back the key\n");
}int main()
{int semid;              // 信号量IDint shmid;              // 共享内存IDkey_t semkey;           // 信号量键值key_t shmkey;           // 共享内存键值char *shmaddr = NULL;   // 指向共享内存段的指针int z = 5;              // 用于生成共享内存键的proj_id值// 使用ftok生成信号量键值,"."表示当前目录,2是proj_id值semkey = ftok(".", 2);  if (semkey == -1) {printf("创建信号量IPC键值失败\n");}// 使用ftok生成共享内存键值,"."表示当前目录,z是proj_id值shmkey = ftok(".", z);  if (shmkey == -1) {printf("创建共享内存IPC键值失败\n");}// 输出生成的共享内存和信号量的键值printf("共享内存IPC键值key = 0x%x\n", shmkey);printf("信号量IPC键值key = 0x%x\n", semkey);// 获取或创建信号量集合,权限0666(所有用户读写)semid = semget(semkey, 1, IPC_CREAT | 0666); if (semid == -1) {printf("创建信号量集合失败\n");exit(-1);           // 如果创建失败,程序退出}// 创建共享内存段,大小为4KB,权限0666(所有用户读写)shmid = shmget(shmkey, 1024 * 4, IPC_CREAT | 0666);  if (shmid == -1) {printf("创建共享内存失败\n");exit(-1);           // 如果创建失败,程序退出}// 初始化信号量为0(相当于上锁)union semun initsem;initsem.val = 0;        // 信号量初始值设为0if (semctl(semid, 0, SETVAL, initsem) == -1) {perror("semctl SETVAL failed");  // 打印错误信息exit(EXIT_FAILURE);}             // 执行P操作,获取信号量pGetkey(semid);// 将共享内存段附加到当前进程的地址空间shmaddr = shmat(shmid, 0, 0);  if (shmaddr == (char *)-1) {perror("shmat failed");exit(EXIT_FAILURE);}// 执行V操作,释放信号量vPutBackKey(semid); printf("shmat ok!\n");  // 提示共享内存附加成功printf("从共享内存中读取出的数据是:%s\n", shmaddr); // 打印共享内存中的数据// 删除信号量集合semctl(semid, 0, IPC_RMID);  // 分离共享内存段shmdt(shmaddr); // 删除共享内存段shmctl(shmid, IPC_RMID, 0); // 打印共享内存IDprintf("Shared memory ID: %d\n", shmid);printf("quit\n");       // 程序结束,输出提示return 0;               // 正常退出程序
}

点个赞吧!!!老铁 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1552931.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

已解决:org.springframework.web.HttpMediaTypeNotAcceptableException

文章目录 写在前面问题描述报错原因分析&#xff1a; 解决思路解决办法1. 确保客户端请求的 Accept 头正确2. 修改 Controller 方法的 produces 参数3. 配置合适的消息转换器4. 检查 Spring 配置中的媒体类型5. 其他解决方案 总结 写在前面 在开发过程中&#xff0c;Spring 框…

C++容器之list基本使用

目录 前言 一、list的介绍&#xff1f; 二、使用 1.list的构造 2.list iterator的使用 3.list capacity &#x1f947; empty &#x1f947;size 4.list element access &#x1f947; front &#x1f947; back 5.list modifiers &#x1f947; push_front &#x1f947; po…

从零到一构建解释器-【1-基础概念】

文章目录 扫描器词法分析语法分析 静态分析中间代码优化代码生成运行时单遍编译器数遍历解释器转译器即使编译编译器与解释器 本教程参考【手搓解释器】 这里只是过一遍基本概念&#xff0c;后面会有涉及到具体解析 扫描器 词法分析 接受字符流忽略无意义符号&#xff0c;如…

【Git】一文看懂Git

Git 一、简介1. Git 与 SVN 区别1.1 Git 是分布式的&#xff0c;SVN 不是1.1.1 分布式版本控制系统Git1.1.2 集中式版本控制系统SVN 1.2 Git 把内容按元数据方式存储&#xff0c;而 SVN 是按文件1.3 Git 分支和 SVN 的分支不同1.4 Git 没有一个全局的版本号&#xff0c;而 SVN …

《Windows PE》3.2.4节表

节表由多个节表项&#xff08;IMAGE_SECTION_ HEADER&#xff09;组成&#xff0c;每个节表项&#xff08;40个字节&#xff09;记录了 PE中与某个特定的节有关的信息&#xff0c;如节的属性、节 的大小、在文件和内存中的起始位置等。节表中节的数量由字段IMAGE_FILE_HEADER. …

迷宫中的最短路径:如何用 BFS 找到最近出口【算法模板】

如何通过广度优先搜索&#xff08;BFS&#xff09;求解迷宫问题 在这篇文章中&#xff0c;我们将学习如何使用 广度优先搜索&#xff08;BFS&#xff09; 解决一个典型的迷宫问题&#xff0c;具体是从迷宫的一个入口出发&#xff0c;找到最近的出口。我们将一步步分析 BFS 是如…

超声波扫描显微镜SAM有什么作用?

知识星球里的学员问&#xff1a;在晶圆厂中很少见到超声波扫描显微镜&#xff0c;但是在封测厂中会经常用到&#xff0c;麻烦讲一下超声波扫描显微镜的原理与用途 什么是超声波扫描显微镜&#xff1f; 超声波扫描显微镜&#xff0c;英文名scanning acoustic microscope&#…

【论文阅读】Equivariant Multi-Modality Image Fusion(CVPR2024)

Equivariant Multi-Modality Image Fusion&#xff08;CVPR2024&#xff09; 现有方法存在的问题 由于现实中没有一种传感器可以同时捕捉所有模态的信息&#xff0c;因此缺乏真实的融合图像作为训练的参照标准&#xff0c;这对深度学习模型的训练带来了挑战。 基于生成对抗网…

2024 全新体验:国学心理 API 接口来袭

在当今快节奏的生活中&#xff0c;人们对于心理健康越来越重视。而研究发现&#xff0c;国学心理学乃至传统文化中的思想智慧&#xff0c;对于人们的心理健康有着独特且深远的影响。为了让更多人能够体验到国学心理的魅力&#xff0c;2024年全新推出的国学心理 API 接口&#x…

基于单片机的两轮直立平衡车的设计

本设计基于单片机设计的两轮自平衡小车&#xff0c;其中机械部分包括车体、车轮、直流电机、锂电池等部件。控制电路板采用STC12C5A60S2作为主控制器&#xff0c;采用6轴姿态传感器MPU6050测量小车倾角&#xff0c;采用TB6612FNG芯片驱动电机。通过模块化编程完成了平衡车系统软…

变电站红外检测数据集 1180张 变电站红外 标注voc yolo 13类

变电站红外检测数据集 1180张 变电站红外 标注voc yolo 13类 变电站红外检测数据集 名称 变电站红外检测数据集 (Substation Infrared Detection Dataset) 规模 图像数量&#xff1a;1185张图像。类别&#xff1a;13种设备类型。标注个数&#xff1a;2813个标注。 数据划分…

关于TF-IDF的一个介绍

在这篇文章中我将介绍TF-IDF有关的一些知识&#xff0c;包括其概念、应用场景、局限性以及相应的代码。 一、概念 TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;是一种广泛用于信息检索和文本挖掘中的统计方法&#xff0c;用于评估一个词在一个文…

鸿蒙ArkUI实战开发-主打自研语言及框架

ArkUI 是 HarmonyOS 的声明式 UI 开发框架&#xff0c;而 ArkUI-X 是基于 ArkUI 框架扩展而来的跨平台开发框架。ArkUI-X 支持 HarmonyOS、OpenHarmony、Android 和 iOS 平台&#xff0c;允许开发者使用一套代码构建支持多平台的应用程序。 一、ArkUI-X 的实战开发步骤 在实战开…

存储主动防御,为什么Gartner技术曲线尤为重视?

【科技明说 &#xff5c; 科技热点关注】 近来&#xff0c;从Gartner发布的2024年存储技术成熟曲线&#xff08;Hype Cycle for Storage Technologies ,2024&#xff09;的相关报告看出&#xff0c;到2028年&#xff0c;所有存储产品都将融入专注于数据主动防御的网络存储功能&…

西电25考研 VS 24考研专业课大纲变动汇总

01专业课变动 西安电子科技大学专业课学长看到953网络安全基础综合变为 893网络安全基础综合&#xff0c;这是因为工科要求都必须是8开头的专业课&#xff0c;里面参考课本还是没变的&#xff0c;无非就是变了一个名字 对于其他变动专业课也是同理的 02专业课考纲内容变化 对于…

深度学习笔记18_TensorFlow实现猫狗识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 一、我的环境 1.语言环境&#xff1a;Python 3.9 2.编译器&#xff1a;Pycharm 3.深度学习环境&#xff1a;TensorFlow 2.10.0 二、GPU设置…

【拥抱AIGC】通义灵码策略配置

通义灵码企业级策配置支持智能问答、行间代码生成安全过滤器相关策略配置。 适用版本 企业标准版、企业专属版 通义灵码管理员、组织内全局管理员&#xff08;专属版&#xff09;在通义灵码控制台的策略配置中进行安全过滤器的配置&#xff0c;开启后&#xff0c;企业内开发…

SOMEIP_ETS_146: SD_ResetInterface

测试目的&#xff1a; 验证DUT在重置后&#xff0c;TestFieldUINT8的值是否至少与重置前设置的值不同&#xff0c;符合SOME/IP规范。 描述 本测试用例旨在确保DUT的ETS能够正确响应重置请求&#xff0c;并且在重置后&#xff0c;特定的测试字段&#xff08;TestFieldUINT8&a…

数据仓库的建设——从数据到知识的桥梁

数据仓库的建设——从数据到知识的桥梁 前言数据仓库的建设 前言 企业每天都在产生海量的数据&#xff0c;这些数据就像无数散落的珍珠&#xff0c;看似杂乱无章&#xff0c;但每一颗都蕴含着潜在的价值。而数据仓库&#xff0c;就是那根将珍珠串起来的线&#xff0c;它能够把…

仅需10G显存,使用 Unsloth 微调 Qwen2 并使用 Ollama 推理

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对大模型技术趋势、算法项目落地经验分享、新手如何入门算法岗、该如何准备面试攻略、面试常考点等热门话题进行了深入的讨论。 总结链接如…