目录
一. 什么是进程间的通信
进程间通信(Inter-Process Communication,简称IPC)是指在操作系统中,不同进程之间进行数据交换和信息传递的机制。在多进程系统中,每个进程都拥有自己独立的内存空间和运行环境,彼此之间无法直接访问对方的数据和资源。为了实现进程之间的数据传输和协作,需要使用进程间通信机制。
常见的进程间通信方式包括:
管道(Pipe):管道是一种半双工的通信方式,可以实现具有亲缘关系的父子进程之间的通信。
命名管道(Named Pipe):命名管道是一种特殊的管道,可以实现在没有亲缘关系的进程之间进行通信。
共享内存(Shared Memory):共享内存是指多个进程共享同一块物理内存区域,在该区域中存储的数据可以被所有共享的进程访问。
消息队列(Message Queue):消息队列是一种可供不相关进程之间进行通信的方式,通过在队列中传递消息实现进程间数据交换。
信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制,通过对信号量的操作来实现进程之间的合作与协调。
套接字(Socket):套接字是一种网络编程中使用的进程间通信方式,可以实现不同主机上的进程之间的通信。
二. 进程间的通信的主要特点
独立性:
每个进程都是独立的,拥有自己独立的地址空间和资源。进程间通信可以实现不同进程之间的数据传输和协作,但不会破坏进程的独立性。
同步与异步:
进程间通信可以是同步的或异步的。同步通信要求发送方等待接收方的响应,直到交换完成后才能继续执行。异步通信允许发送方和接收方并行进行其他任务,不需要等待对方响应。
缓冲区管理:
进程间通信可能使用缓冲区来存储待发送或已接收的数据。发送方将数据写入缓冲区后可以继续执行,而接收方从缓冲区读取数据时,可以根据需要进行处理。
可靠性:
进程间通信可以是可靠的或不可靠的,取决于通信机制和应用需求。可靠通信确保数据的正确传输,具备错误检测和重传机制。而不可靠通信可能丢失数据或出现乱序等问题。
数据格式:
进程间通信需要定义数据的格式和协议,以确保发送方和接收方能够正确解析和处理数据。常用的数据格式包括二进制、文本、JSON等。
三. 无名管道
无名管道(Pipe)是一种轻量级的进程间通信(IPC)机制。它通过内核提供的特殊文件来实现,用于在父子进程或兄弟进程之间传递数据。
无名管道的创建通过系统调用
pipe()
来完成,它会返回两个文件描述符:一个用于写入端,另一个用于读取端。这两个文件描述符可以分别用于向管道写入数据和从管道读取数据,实现进程之间的单向通信。
3.1 无名管道的特点
单向通信:无名管道是单向的,仅支持一个进程向另一个进程发送数据。其中一个进程充当写入端(Writer),另一个进程充当读取端(Reader)。
基于文件描述符:无名管道是通过使用文件描述符来进行读写操作的。在创建管道后,会为该管道分配两个文件描述符,一个用于写入端,另一个用于读取端。
父子进程通信:无名管道通常用于父子进程间通信,因为它们共享同一个进程空间。父进程可以创建管道,并将其传递给子进程,从而实现二者之间的数据传输。
有限容量:无名管道具有有限的容量,通常是几千字节。一旦管道被写满,继续写入数据的进程将被阻塞,直到有其他进程读取了管道中的数据。
非持久化:无名管道只存在于创建它的进程运行期间,在进程终止后会被自动销毁。因此,它适用于临时性的进程间通信需求。
3.2 pipe函数
头文件 #include<unistd.h> 函数原型 int pipe(int fd[2]); 参数 fd[2]:
经参数fd返回的两个文件描述符
fd[0]为读而打开,fd[1]为写而打开
fd[1]的输出 是 fd[0]的输入
作用 可用于创建一个管道,以实现进程间的通信。 返回值 成功:返回0
失败:返回-1
注意:
当一个管道建立时,它会创建两个文件描述符:
fd[0]
为读而打开,fd[1]
为写而打开
要关闭管道,只需将这两个文件描述符关闭即可。
示例代码:
#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { //创建一个无名管道,只能在亲戚进程间通信 //int pipe(int pipefd[2]); int fd[2]; if(pipe(fd) == -1) printf("create the pipe failed\n"); int pid = 0; char buf[128]; pid = fork(); if(pid > 0) { sleep(3); printf("this is parent process\n"); close(fd[0]);//关闭读端 write(fd[1],"hello world",strlen("hello world"));//向写端写入数据 wait(NULL);//等待子进程退出 } else { printf("this is subprocess\n"); close(fd[1]);//关闭写端 read(fd[0],buf,128);//管道内无数据会阻塞在这,然后执行父进程 printf("the data is %s \n",buf); exit(0);//子进程退出 } return 0; }
细节: 在写的时候关闭读端,在读的时候关闭写端。
四. 命名管道
命名管道(FIFO)是一种进程间通信(IPC)机制,它通过内核提供的特殊文件来实现,在不同进程之间传递数据。相对于无名管道,命名管道可以在多个进程之间进行双向通信,而且不限于父子进程或兄弟进程之间的通信。
创建命名管道可以通过系统调用mkfifo()来完成,它会在指定的路径下创建一个特殊的文件,并返回一个文件描述符。通过这个文件描述符,我们可以向命名管道写入数据或从命名管道读取数据。
由于命名管道是基于文件系统的,多个进程可以通过打开相同的文件路径来访问同一个管道,实现进程之间的通信。这样的通信方式可以在进程之间实现数据共享和协作,提供了一种方便和可靠的 IPC 机制。
4.1 命名管道的特点
文件系统基础:命名管道是基于文件系统的一种进程间通信机制。它通过在文件系统中创建一个特殊的文件来实现通信。这意味着可以使用文件操作的方式来读取和写入命名管道,使得通信过程更加简单和直观。
多进程访问:与无名管道不同,命名管道可以被多个进程同时访问。多个进程可以通过打开相同的管道文件路径来进行通信。这样的设计允许了更灵活的进程间通信方式,不局限于父子进程或兄弟进程之间的通信。
双向通信:命名管道支持双向通信。即在同一个管道中既可以进行读取数据,也可以进行写入数据。不同的进程可以通过共享同一个管道来进行双向的数据传输,实现进程之间的交互与合作。
持久性:命名管道是持久存在的,即使在创建它的进程结束后,管道仍然可以继续存在,供其他进程使用。这一特点使得不同进程可以在不同时刻进行通信,提高了通信的灵活性和可靠性。
高效性:由于基于文件系统实现,系统可以充分利用操作系统内核的缓存机制,使数据传输更加快速和高效。
4.2 mkfifo函数
头文件 #include<sys/types.h>
#include<sys/stat.h>
函数原型 int mkfifo(const char *pathname, mode_t mode); 参数 pathname:
创建管道名字,一般放在/tmp/
mode:
管道权限,0600可读可写
作用 创建一个命名管道,并返回一个文件描述符。 返回值 成功:返回0
失败:返回-1
注意:
命名管道是一种特殊的文件类型,它和普通文件不同之处在于,对于读取进程来说,如果没有数据可读,它会被阻塞直到有数据可读。同样,对于写入进程来说,如果管道已满,写入操作也会被阻塞。需要确保正确的读写顺序,避免造成死锁或数据丢失的情况。
当使用open函数打开一个命名管道时,它的阻塞行为取决于是否指定了O_NONBLOCK标志。
如果没有指定O_NONBLOCK(默认情况),当调用只读模式(O_RDONLY)的open函数时,如果当前没有其他进程以写模式(O_WRONLY或O_RDWR)打开同一个命名管道,读取操作将被阻塞。也就是说,读取操作会一直等待,直到有其他进程以写模式打开该管道为止。类似地,当调用只写模式(O_WRONLY)的open函数时,如果当前没有其他进程以读模式打开同一个命名管道,写入操作将被阻塞,它会等待直到有其他进程以读模式打开该管道为止。
如果指定了O_NONBLOCK标志,当调用只读模式的open函数时,即使当前没有其他进程以写模式打开同一个命名管道,读取操作也会立即返回。这种情况下,如果没有数据可读,读操作将返回0表示没有读取到数据。而对于只写模式的open函数,如果当前没有其他进程以读模式打开同一个命名管道,写入操作会出错并返回-1,同时设置errno为ENXIO,表示No such device or address(设备地址不存在),即无法找到读取端口。
示例代码:
/*读端*/ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> int main() { char buf[128] = {0}; //创建有名管道,可以在不同的进程下通信,创建后生成特殊的设备文件 //另外一种写法,创建失败才会进入if语句 if(mkfifo("./file",0600) == -1 && errno != EEXIST) { printf("create fail\n"); } //若没有指定O_NONBLOCK,只读open会阻塞,等到别的进程写入才打开 int fp = open("./file",O_RDONLY); int nread = read(fp,buf,128); //读操作 printf("the buf is %d byte , content is %s\n",nread,buf); return 0; }
/*写端*/ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main() { char *str = "hello"; int fp = open("./file",O_WRONLY);//只写的方式打开,可以使读的进程不阻塞 int nwrite = write(fp,str,strlen(str)); //写操作 printf("write success\r\n"); return 0; }
五. 消息队列
消息队列是一种进程间通信(IPC)机制,它允许不同进程之间通过内核提供的特殊数据结构进行异步通信。相对于管道,消息队列可以实现更灵活和可靠的通信方式,并且不限于父子进程或兄弟进程之间的通信。
创建消息队列可以使用系统调用msgget()来完成,它会在内核中创建一个消息队列并返回一个唯一的标识符(消息队列ID)。通过这个标识符,进程可以向消息队列发送消息或从消息队列接收消息。
消息队列中的每条消息由一个特定的消息类型、一个优先级以及一个数据块组成。发送进程可以指定消息类型,并将数据发送到消息队列。接收进程可以按照指定的消息类型从消息队列中读取消息,并处理消息中的数据。
5.1 消息队列的特点
高效性:Linux消息队列通过共享内存实现,数据的读写操作在内核空间完成,避免了用户空间和内核空间之间的频繁切换,提高了通信的效率。
容量可调:Linux消息队列的容量可根据实际需求进行调整。可以通过系统调用
msgctl()
函数来设置消息队列的最大字节数,并且可以动态修改。多对多通信:消息队列支持多个进程之间的通信,一个进程可以同时与多个进程进行通信,而不需要为每个进程创建单独的通信通道。
异步通信:发送者将消息发送到消息队列后即可继续其他操作,不需要等待接收者处理完毕。接收者可以根据需要选择合适的时机从队列中读取消息,实现异步通信。
消息优先级:Linux消息队列允许为消息设置优先级,接收者可以根据消息的优先级来选择处理消息的顺序,提高系统的实时性和灵活性。
持久化:Linux消息队列允许设置消息的持久化属性,即使在系统重启后,之前未被接收的消息仍然可以在队列中保持,确保消息的不丢失。
5.2 消息队列的相关函数
5.2.1 msgget函数
头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型 int msgget(key_t key, int msgflg) 参数 key:
0:会建立新的消息队列
大于0的32位整数:视参数msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值
msgflg:
0:取消息队列标识符,若不存在则函数会报错
IPC_CREAT:当msgflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错
作用 得到消息队列标识符或创建一个消息队列对象 返回值 成功:返回消息队列的标识符
失败:返回-1
注意:
msgflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定消息队列的存取权限。
5.2.2 msgctl函数
头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型 int msgctl(int msqid, int cmd, struct msqid_ds *buf) 参数 msqid:
消息队列标识符
cmd:
IPC_STAT:获得msgid的消息队列头数据到buf中
IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
buf:
消息队列管理结构体,请参见消息队列内核结构说明部分
作用 获取和设置消息队列的属性 返回值 成功:返回0
失败:返回-1
5.2.3 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) 参数 msqid:
消息队列标识符
msgp:
发送给队列的消息。msgp可以是任何类型的结构体,但第一个字段必须为long类型,即表明此发送消息的类型,msgrcv根据此接收消息。
msgsz:
要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度
msgflg:
0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。
作用 将msgp消息写入到标识符为msqid的消息队列 返回值 成功:返回0
失败:返回-1
注意:
msgsnd()为阻塞函数,当消息队列容量满或消息个数满会阻塞。消息队列已被删除,则返回EIDRM错误;被信号中断返回E_INTR错误。
如果设置IPC_NOWAIT消息队列满或个数满时会返回-1,并且置EAGAIN错误。
msgsnd()解除阻塞的条件有以下三个条件:
① 不满足消息队列满或个数满两个条件,即消息队列中有容纳该消息的空间。
② msqid代表的消息队列被删除。
③ 调用msgsnd函数的进程被信号中断。
5.2.4 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);
参数 msqid:
消息队列标识符
msgp:
存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同
msgsz:
要接收消息的大小,不含消息类型占用的4个字节
msgtyp:
0:接收第一个消息
>0:接收类型等于msgtyp的第一个消息
<0:接收类型等于或者小于msgtyp绝对值的第一个消息
msgflg:
0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃
作用 从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除 返回值 成功:实际读取到的消息数据长度
失败:返回-1
注意:
msgrcv()解除阻塞的条件有以下三个:
① 消息队列中有了满足条件的消息。
② msqid代表的消息队列被删除。
③ 调用msgrcv()的进程被信号中断。
5.2.5 查看 / 删除内核的消息队列的命令
ipcs -q 查看内核中的消息队列 ipcrm -q msqid 删除内核中的消息对列
5.2.6 示例代码
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <string.h> struct Msg { long type; char buf[1024]; }; int main() { key_t key; // key_t ftok(const char *pathname, int proj_id); //获取key值,第一个参数是目前目录,第二个参数是子序号(可以随便) key = ftok(".",'z'); printf("key:%x\n",key); /*使用消息队列,进行相互通信操作*/ struct Msg readbuf; struct Msg sendbuf = {988,"you are very handsome!"}; //访问一个消息队列或创建一个消息队列设置权限,并返回消息队列的标志符 //int msgget(key_t key, int msgflg); int msgId = msgget(key,IPC_CREAT|0777); if(msgId == -1) printf("create the queue is error\n"); //从消息队列读取信息,每读取一条少一条,否则阻塞等待 // ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); //参数5,0: 阻塞式接收消息 msgrcv(msgId,&readbuf,sizeof(readbuf.buf),888,0); printf("the content is %s\n",readbuf.buf); msgsnd(msgId,&sendbuf,strlen(sendbuf.buf),988); printf("send success!\n"); //int msgctl(int msqid, int cmd, struct msqid_ds *buf); //将消息队列链表从内核中删除 msgctl(msgId,IPC_RMID,NULL); return 0; }
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <string.h> struct Msg { long type; char buf[1024]; }; int main() { key_t key; // key_t ftok(const char *pathname, int proj_id); //获取key值,第一个参数是目前目录,第二个参数是子序号(可以随便) key = ftok(".",'z'); printf("key:%x\n",key); /*使用消息队列,进行相互通信操作*/ struct Msg readbuf; struct Msg sendbuf = {888,"hello world!"}; //访问一个消息队列或创建一个消息队列设置权限,并返回消息队列的标志符 int msgId = msgget(key,IPC_CREAT|0777); if(msgId == -1) printf("create the queue is error\n"); //将消息写入到消息队列 //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); msgsnd(msgId,&sendbuf,strlen(sendbuf.buf),888); printf("send success!\n"); msgrcv(msgId,&readbuf,sizeof(readbuf.buf),988,0); printf("the content is %s\n",readbuf.buf); //int msgctl(int msqid, int cmd, struct msqid_ds *buf); //将消息队列链表从内核中删除 msgctl(msgId,IPC_RMID,NULL); return 0; }
六. 共享内存
共享内存是一种进程间通信(IPC)机制,它允许多个进程共享同一块内存区域,从而实现高效的数据交换和通信。在共享内存中,多个进程可以通过共享相同的物理内存页来进行数据的读写操作,避免了数据的复制和传输过程,提高了通信的效率。由于多个进程可以直接访问同一块内存区域,因此共享内存是一种高速、低延迟的通信方式。
创建共享内存可以使用系统调用shmget()来完成,它会在内核中创建一个共享内存对象,并返回一个唯一的标识符(共享内存ID)。通过这个标识符,进程可以对共享内存进行操作,包括映射到自己的虚拟地址空间、读写数据等。
需要注意的是,共享内存需要进程之间进行同步和互斥操作,以免多个进程同时读写共享内存导致数据不一致或竞争条件的问题。常见的同步机制包括使用信号量(Semaphore)来控制对共享内存的访问,确保每次只有一个进程在读写共享内存。
6.1 共享内存的特点
高效性:共享内存是最快速的进程间通信方式之一。由于数据直接存储在物理内存中,进程可以直接访问共享内存,避免了数据复制和传输的开销,因此读写操作非常高效。
多对多通信:共享内存支持多个进程同时访问同一块共享内存区域。不同进程可以通过共享内存进行数据交流和协作,实现多对多的通信模式。
易用性:使用共享内存相对简单。通过系统调用shmget()创建共享内存并获取共享内存标识符,然后将共享内存映射到进程的地址空间,就可以实现对共享内存的读写操作。
生命周期独立:共享内存的生命周期不依赖于创建它的进程。即使创建共享内存的进程终止,其他进程仍可以继续访问和使用共享内存区域,直到显示地删除共享内存。
同步与互斥:由于共享内存是多个进程共享的,需要考虑进程间并发访问的同步和互斥问题。可以使用信号量、互斥锁等同步机制来保证数据的正确性和一致性。
6.2 共享内粗的相关函数
6.2.1 shmget函数
头文件 #include <sys/ipc.h>
#include <sys/shm.h>
函数原型 int shmget(key_t key, size_t size, int shmflg) 参数 key:
0:会建立新共享内存对象
大于0的32位整数:视参数shmflg来确定操作。通常要求此值来源于ftok返回的IPC键值
size:
大于0的整数:新建的共享内存大小,以字节为单位
0:只获取共享内存时指定为0
shmflg:
0:取共享内存标识符,若不存在则函数会报错
IPC_CREAT:如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符
IPC_CREAT|IPC_EXCL:如果内核中不存在键值 与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错
作用 得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符 返回值 成功:返回共享内存的标识符
失败:返回-1
注意:
shmflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定信号量集的存取权限。
6.2.2 shmat函数
头文件 #include <sys/types.h>
#include <sys/shm.h>
函数原型 void *shmat(int shmid, const void *shmaddr, int shmflg) 参数 shmid:
共享内存标识符。
shmaddr:
指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
shmflg:
SHM_RDONLY:为只读模式,其他为读写模式
作用 连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问 返回值 成功:附加好的共享内存地址
失败:返回-1
6.2.3 shmdt函数
头文件 #include <sys/types.h>
#include <sys/shm.h>
函数原型 int shmdt(const void *shmaddr) 参数 shmaddr:
连接的共享内存的起始地址。
作用 与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存 返回值 成功:返回0
失败:返回-1
注意:
本函数调用并不删除所指定的共享内存区,而只是将先前用shmat函数连接(attach)好的共享内存脱离(detach)目前的进程。
6.2.4 shmctl函数
头文件 #include <sys/types.h>
#include <sys/shm.h>
函数原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf) 参数 shmid:
共享内存标识符。
cmd:
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内。
IPC_RMID:删除这片共享内存。
buf:
共享内存管理结构体。
作用 完成对共享内存的控制 返回值 成功:返回 0
失败:返回-1
6.2.5 查看 / 删除本机的共享内存
ipcs -m 查看本机的所有共享内存 ipcrm -m shmid 删除对应的共享内存
6.2.6 示例代码
/*读端*/ #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { key_t key; key = ftok(".",1); //int shmget(key_t key, size_t size, int shmflg); //创建/打开一个共享内存,以字节为单位的内存 int shmId; shmId = shmget(key,1024*4,0); if(shmId == -1) { printf("creat the shm is failed\n"); exit(-1); } else printf("success! the shmId is %d\n",shmId); //把共享内存区映射到调用进程的地址空间 //void *shmat(int shmid, const void *shmaddr, int shmflg); /* 第二个参数NULL是让系统自动找寻合适的地址 第三个参数是0,以可读可写的方式连接这个共享内存 */ char *shmaddr = NULL; shmaddr = shmat(shmId,NULL,0); printf("the shm content is %s\n",shmaddr); //从共享内存读数据时,必须要在这延时的5秒(有一个延迟)内进行, //否则共享内存将被释放,无法与共享内存建立联系。 sleep(5); //把共享内存区从调用进程的地址空间断开 // int shmdt(const void *shmaddr); shmdt(shmaddr); //将共享内存进行删除 //int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmctl(shmId,IPC_RMID,NULL); printf("done!\n"); return 0; }
/*写端*/ #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main() { key_t key; key = ftok(".",1); //创建/打开一个共享内存,以字节为单位的内存 //int shmget(key_t key, size_t size, int shmflg); //创建一个的共享内存,权限是可读可写可执行 int shmId; shmId = shmget(key,1024*4,IPC_CREAT|0777); if(shmId == -1) { printf("creat the shm is failed\n"); exit(-1); } else printf("success! the shmId is %d\n",shmId); //把共享内存区映射到调用进程的地址空间 //void *shmat(int shmid, const void *shmaddr, int shmflg); /* 第二个参数NULL是让系统自动找寻合适的地址 第三个参数是0,以可读可写的方式连接这个共享内存 */ char *shmaddr = NULL; shmaddr = shmat(shmId,NULL,0); //将数据写入共享内存 strcpy(shmaddr,"hello world!"); //从共享内存读数据时,必须要在这延时的5秒(有一个延迟)内进行, //否则共享内存将被释放,无法与共享内存建立联系。 sleep(5); //把共享内存区从调用进程的地址空间断开 // int shmdt(const void *shmaddr); shmdt(shmaddr); printf("done!\n"); return 0; }
七. 信号
信号是一种进程间通信(IPC)方式,它允许操作系统向特定进程或进程组发送信号,从而使进程能够响应系统的某些事件或异常情况。
每个信号都有一个唯一的整数编号,并以“SIG”开头命名,例如“SIGKILL”、“SIGINT”等。信号编号从1开始,不存在0号信号。信号定义在signal.h头文件中。
要查看可用的信号名称和编号,可以使用kill -l命令。该命令会列出所有可用的信号名称和对应的编号。kill命令也可以使用信号编号来发送信号。需要注意的是,kill命令向进程或进程组发送的信号如果是0,则不执行任何操作,但可以检查目标进程是否存在。
在Linux中,信号可以被发送到指定的进程或进程组,并且可以由接收信号的进程拦截和处理。进程可以通过signal()或者sigaction()函数来注册信号处理函数,以实现对特定信号的自定义处理方式。需要注意的是,在处理信号时应该尽可能快速地完成信号处理任务,以免影响进程性能和稳定性。
7.1 信号的处理方式
忽略信号(Ignore):进程可以选择忽略某个特定的信号,即当接收到该信号时,不做任何处理。这可以通过将信号处理函数设置为SIG_IGN来实现。忽略信号可能会导致信号被丢弃,进而无法被处理。因此,需要谨慎使用忽略信号,只有在特定情况下才适用。有些信号不能被忽略,例如SIGKILL和SIGSTOP。
捕捉信号(Catch):进程可以为特定的信号注册自定义的信号处理函数,用于在接收到信号时执行特定的操作。通过signal()或者sigaction()函数可以实现信号处理函数的注册。捕捉信号可以根据具体的需求做出相应的处理操作,例如记录日志、释放资源或者更新状态等。
默认动作(Default Action):每个信号都有一个默认的处理动作,即当没有为信号注册自定义的信号处理函数时,系统会采取默认的处理方式。常见的默认处理方式包括终止进程、终止并生成核心转储文件、挂起进程等。具体的默认处理方式可以通过kill -l命令查看。
7.2 信号的相关函数
7.2.1 signal函数
头文件 #include<signal.h> 函数原型 void (*signal(int signum,void(* handler)(int)))(int); 参数 signum:
指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
handler:
描述与信号关联的动作,
它可以取以下三种值:
一个无返回值的函数地址:此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。
SIG_IGN:这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。
SIG_DFL:这个符号表示恢复系统对信号的默认处理。
作用 设置某一信号的对应动作 返回值 成功:先前的信号处理函数指针
失败:返回-1
示例代码:
#include <signal.h> #include <stdio.h> void handler(int signum) { printf("the signum is %d\n",signum); switch(signum) { case 2: printf("SIGINT\n"); break; case 10: printf("SIGUSR1\n"); break; case 7: printf("SIGBUS\n"); break; } } int main() { /* typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); */ //注册一个信号捕捉函数,将捕捉的信号,改成别的处理方式 //在终端发送信号:kill -9 (信号编号) signal(SIGINT,SIG_IGN); //SIG_IGN(忽略信号) signal(SIGINT,handler); signal(SIGUSR1,handler); signal(SIGBUS,handler); while(1); return 0; }
7.2.2 sigaction函数
#include <signal.h> int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
说明:
sa_sigaction:如果sa_flags设置了SA_SIGINFO标志位,则会使用sa_sigaction处理函数,否则使用sa_handler处理函数。
sa_mask:定义一组信号,在调用由sa_handler所定义的处理器程序时将阻塞该组信号,不允许它们中断此处理器程序的执行。
sa_flags:位掩码,指定用于控制信号处理过程的各种选项。示例代码:
/*接收信号*/ #include <stdio.h> #include <signal.h> #include <sys/types.h> #include <unistd.h> void handler(int signum,siginfo_t *siginfo, void * content) { printf("signum is %d\n",signum); if(content != NULL) { printf("from pid is %d\n",siginfo->si_pid); printf("value is %d\n", siginfo->si_int); } } /* struct sigaction { //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作 void (*sa_handler)(int); //信号处理程序,能够接受额外数据和sigqueue配合使用 void (*sa_sigaction)(int, siginfo_t *, void *); //阻塞关键字的信号集,可以再调用捕捉函数之前, //把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。 sigset_t sa_mask; //影响信号的行为SA_SIGINFO表示能够接受数据 int sa_flags; }; 回调函数句柄sa_handler、sa_sigaction只能任选其一 */ int main() { printf("the pid is %d\n",getpid()); //打印当前的进程号 struct sigaction msg; //sigaction第二个参数结构体编写 msg.sa_sigaction = handler; //函数 msg.sa_flags = SA_SIGINFO; //表示信号处理携带信息 //int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact); //因为结构体中有sa_mask 未收到信号自动阻塞 sigaction(SIGUSR1,&msg,NULL); //第三个参数不备份用NULL while(1); return 0; }
/*发送信号*/ #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> /* union sigval { int sival_int; void *sival_ptr; }; */ int main(int argc,char **argv) { //发送信号给调用线程。 // int sigqueue(pid_t pid, int sig, const union sigval value); int pid; int sig; pid = atoi(argv[2]); sig = atoi(argv[1]); union sigval sigv; sigv.sival_int = 520; sigqueue(pid,sig,sigv); //携带的消息在联合体中 printf("%d done!\n",getpid()); return 0; }
八. 信号量
信号量可以看作是一个计数器,它的值表示可供使用的资源数量。主要用于解决多个进程(或线程)之间的竞态条件和临界区问题,确保资源在不同的进程之间被正确地共享和访问。
信号量的基本操作有两个:P操作(也称为申请操作)和V操作(也称为释放操作)。P操作用于申请资源,它会试图将信号量的值减1,当信号量的值大于等于0时,表示有可用的资源,进程可以继续执行;当信号量的值为负数时,表示资源不足,进程需要等待其他进程释放资源。V操作用于释放资源,它会将信号量的值加1,表示资源已经被释放,并唤醒等待的进程。
除了常用的二进制信号量(取值0或1),还有计数信号量,它允许设置初始值,并可以取任意非负整数值。计数信号量可以用于表示一组可用的资源数量,进程可以根据需要申请或释放多个资源。
8.1 信号量的特点
计数器形式:信号量可以看作是一个计数器,用于记录可用的资源数量。它的值表示当前可供使用的资源个数,可以是整数形式的非负值。
互斥性:信号量可以实现对共享资源的互斥访问,确保同一时间只有一个进程(或线程)可以访问资源。通过申请和释放操作,进程可以协调访问顺序,避免冲突和数据不一致。
同步性:信号量也可以用于进程或线程之间的同步操作,确保它们按照特定的顺序执行。通过等待和唤醒操作,可以实现进程间的同步和协调,使得多个进程能够有序地执行。
阻塞与非阻塞:在申请资源时,如果信号量的值为0,表示资源已被占用,进程可以选择等待(阻塞)或继续执行(非阻塞)。当资源可用时,进程会被唤醒并继续执行。这种机制可以有效地控制进程的执行顺序和调度。
可以用于多种资源:信号量不仅可以用于控制共享的临界资源,还可以用于控制对文件、内存缓冲区、设备等资源的访问。通过适当的设置和管理,可以实现多种资源的合理分配和共享。
8.2 信号量的相关函数
//头文件 #include <sys/sem.h> // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1 //当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1 int semget(key_t key, int num_sems, int sem_flags); // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1 int semop(int semid, struct sembuf semoparray[], size_t numops); struct sembuf { short sem_num; // 信号量组中对应的序号,0~sem_nums-1 short sem_op; // 信号量值在一次操作中的改变量 short sem_flg; // IPC_NOWAIT, SEM_UNDO } // 控制信号量的相关信息 int semctl(int semid, int sem_num, int cmd, ...);
示例代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> //联合体,用于semctl初始化 union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ }; void PgetKey(int id) { //int semop(int semid, struct sembuf *sops, size_t nsops); struct sembuf set; set.sem_num = 0; //信号量编号 set.sem_op = -1; //拿钥匙(-1) set.sem_flg = SEM_UNDO; //无钥匙,等待 semop(id,&set,1); printf("P: GET THE KEY\n"); } void VputKey(int id) { //int semop(int semid, struct sembuf *sops, size_t nsops); struct sembuf set; set.sem_num = 0; //信号量编号 set.sem_op = 1; //放钥匙(+1) set.sem_flg = SEM_UNDO; //无钥匙,等待 semop(id,&set,1); printf("V: PUT THE KEY\n"); } int main() { key_t key = ftok(".",1); //创建一个信号量 //int semget(key_t key, int nsems, int semflg); int semid = 0; semid = semget(key,1,IPC_CREAT|0666); //参数二:1代表信号量集合中有一个信号量 union semun initsem; initsem.val = 0;//代表信号量的值 //初始化信号量 //int semctl(int semid, int semnum, int cmd, ...); semctl(semid,0,SETVAL,initsem); //参数二:0代表操作第0个信号量 //SETVAL:设置信号量的值,设置为initsem //创建子进程 int pid = fork(); if(pid > 0) { PgetKey(semid); printf("this is parent process\n"); VputKey(semid); } else if(pid == 0) { printf("this is subprocess\n"); VputKey(semid); } else { printf("fork error\n"); } return 0; }