进程间通信(IPC)全称 Interprocess communication ,进程通信就是在不同进程间传播或者交换信息
让不同进程看到同一块内存,(同一块资源)
在之前的 Linux进程概念中我们谈到 :进程具有独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰。,这在一定程度上加大了进程通信的难度。所以,我们通常借助第三方资源来进行数据通信。
我们把从一个进程连接到另一个进程的一个数据流称为管道,我们比较常见的 " | "就是典型的管道。
示例:
who | wc -l# 作用 统计我们当前使用云服务器上的登录用户个数。
让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
pipe函数
参数解释:
pipefd[0] :表示管道读端的文件描述符;pipefd[1] : 表示管道写端的文件描述符
匿名管道使用步骤
1. 创建管道(父进程);
2. 创建子进程;
3.父进程关闭读端,子进程关闭写端
1、管道内部自带同步与互斥机制。(不用担心安全问题)
我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。为了保护管道资源,内核会对管道操作进行同步与互斥。
2、管道的生命周期随进程。
管道依赖文件系统,只要当打开管道的所有进程退出后,管道文件才会退出
3、管道提供的是流式服务。
管道提供的服务是管道文件中有多少就获取多少,(写多少读多少),这种服务属于流式服务
4、管道是半双工通信的。
我们之前提到,管道只能提供单向服务,只能单独“ 读 ” 或者单独 “ 写 ” ,不能同时 “ 读写 ” 。这种通信叫做 半双工通信,与之相对的是全双工通信,即 可以同时读写,这样虽然效率好像高了,但是也要求对临界资源有其他的保护手段。
1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
上述两种情况内核中可以通过调节同步与互斥来调节这种读写不均的问题。
3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
没有数据可以读了,写段关闭后,读端结束读写逻辑去做之后的代码
4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
为什么这种情况不会像第二种一样呢? ---读端已经关闭了,没有操作可以读了,os只能停止写端了,这种很明显是程序异常退出,那这种异常是什么呢。我们通过这个实验来观察
- #include
- #include
- #include
- #include
- #include
- #include
- int main()
- {
- int fd[2] = { 0 };
- if (pipe(fd) < 0){ //使用pipe创建匿名管道
- perror("pipe");
- return 1;
- }
- pid_t id = fork(); //使用fork创建子进程
- if (id == 0){
- //child
- close(fd[0]); //子进程关闭读端
- //子进程向管道写入数据
- const char* msg = "hello father, I am child...";
- int count = 10;
- while (count--){
- write(fd[1], msg, strlen(msg));
- sleep(1);
- }
- close(fd[1]); //子进程写入完毕,关闭文件
- exit(0);
- }
- //father
- close(fd[1]); //父进程关闭写端
- close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
- int status = 0;
- waitpid(id, &status, 0);
- printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
- return 0;
- }
13号信号对应的是 SIGPIPE 信号
我们可以通过 man 7 signal 去查看信号的具体解释
通过上面几种情况的分析,我们不难得出管道是有大小限制的,我们可以通过以下两种方式查看管道大小:
1. ulimit -a
2. man 7 pipe
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
mkfifo fifo
使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
- while : ; do echo "hello";sleep 1;done > fifo
-
-
- cat < fifo
这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,
在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
我们通过使用函数 mkfifo函数创建命名管道
函数介绍:
参数解释:
path name:要创建的命名管道文件。
mode: 建命名管道文件的默认权限
示例:
- #include
- #include
- #include
-
- #define FILE_NAME "myfifo"
-
- int main()
- {
- umask(0); //将文件默认掩码设置为0
- if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
- perror("mkfifo");
- return 1;
- }
-
- //create success...
-
- return 0;
- }
1、如果当前打开操作是为读而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
server端
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
-
-
- int main()
- {
- umask(0); //将文件默认掩码设置为0
- if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
- perror("mkfifo");
- return 1;
- }
- int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
- if (fd < 0){
- perror("open");
- return 2;
- }
- char msg[128];
- while (1){
- msg[0] = '\0'; //每次读之前将msg清空
- //从命名管道当中读取信息
- ssize_t s = read(fd, msg, sizeof(msg)-1);
- if (s > 0){
- msg[s] = '\0'; //手动设置'\0',便于输出
- printf("client# %s\n", msg); //输出客户端发来的信息
- }
- else if (s == 0){
- printf("client quit!\n");
- break;
- }
- else{
- printf("read error!\n");
- break;
- }
- }
- close(fd); //通信完毕,关闭命名管道文件
- return 0;
- }
client端
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
-
-
- int main()
- {
- int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
- if (fd < 0){
- perror("open");
- return 1;
- }
- char msg[128];
- while (1){
- msg[0] = '\0'; //每次读之前将msg清空
- printf("Please Enter# "); //提示客户端输入
- fflush(stdout);
- //从客户端的标准输入流读取信息
- ssize_t s = read(0, msg, sizeof(msg)-1);
- if (s > 0){
- msg[s - 1] = '\0';
- //将信息写入命名管道
- write(fd, msg, strlen(msg));
- }
- }
- close(fd); //通信完毕,关闭命名管道文件
- return 0;
- }
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。
1. 调用shmget ()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
2. 使用shmat ()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
3. 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat ()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
4. 调用shmdt ()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
5. 调用shmctl ()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
使用示例:
服务端
- //server.c
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名
-
- #define PROJ_ID 0x6666 //整数标识符
- #define SIZE 4096 //共享内存的大小
-
-
-
- int main()
- {
- key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
- if (key < 0){
- perror("ftok");
- return 1;
- }
-
- int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
- if (shm < 0){
- perror("shmget");
- return 2;
- }
-
- printf("key: %x\n", key); //打印key值
- printf("shm: %d\n", shm); //打印共享内存用户层id
-
- char* mem = shmat(shm, NULL, 0); //关联共享内存
-
- while (1){
- //不进行操作
- }
-
- shmdt(mem); //共享内存去关联
-
- shmctl(shm, IPC_RMID, NULL); //释放共享内存
- return 0;
- }
客户端:
- //client.c
- //comm.h
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名
-
- #define PROJ_ID 0x6666 //整数标识符
- #define SIZE 4096 //共享内存的大小
-
-
-
- int main()
- {
- key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
- if (key < 0){
- perror("ftok");
- return 1;
- }
- int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
- if (shm < 0){
- perror("shmget");
- return 2;
- }
-
- printf("key: %x\n", key); //打印key值
- printf("shm: %d\n", shm); //打印共享内存用户层id
-
- char* mem = shmat(shm, NULL, 0); //关联共享内存
-
- int i = 0;
- while (1){
- //不进行操作
- }
-
- shmdt(mem); //共享内存去关联
- return 0;
- }