Linux操作系统进程间通信
Posted Ricky_0528
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统进程间通信相关的知识,希望对你有一定的参考价值。
文章目录
1. 进程间通信介绍
进程之间会出现协同工作的场景,一个进程需要把自己的数据交付给另一个进程,让其处理,这就产生了进程间通信,因此操作系统需要来设计通信方式
进程之间是具有独立性的,一个进程无法看到另一个进程的资源,因此交互数据会比较麻烦,两个进程要想相互通信,就得先看到一份公共的资源,也就是一段内存,这段内存属于操作系统
进程间通信的本质:由操作系统参与,提供一份所有通信进程能看到的公共资源,这可能以文件方式提供,可以能是队列的方式,也可能就是原始的内存块,这也就产生了多种通信方式
1.1 进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.2 进程间通信的发展
- 管道
- System V进程间通信
- POSIX进程间通信
1.3 进程间通信分类
- 管道
- 匿名管道pipe
- 命名管道
- System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
2. 管道
2.1 匿名管道
基于文件的通信方式——管道
管道可以理解为文件的内核缓冲区,它为父子进程所共享的一块内存
匿名管道特点:
- 是一个只能单向通信的通信管道
- 管道是面向字节流的
- 仅限于父子进程之间通信
- 管道自带同步机制,原子性写入
- 管道的生命周期是随进程的
匿名管道通信实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
int pipefd[2] = 0;
if (pipe(pipefd) != 0)
perror("pipe error!");
return 1;
// 0:对应读取端 1:对应写入端
printf("pipefd[0]: %d\\n", pipefd[0]);
printf("pipefd[1]: %d\\n", pipefd[1]);
// 让父进程进行读取,子进程进行写入
if (fork() == 0)
// 子进程
close(pipefd[0]);
const char *msg = "hello\\n";
while (1)
// pipe只要有缓冲区就会一直写入
write(pipefd[1], msg, strlen(msg)); // 这里不需要+1,\\n是C语言级别的东西
sleep(1);
exit(0);
// 父进程
close(pipefd[1]);
while (1)
// 没有让父进程sleep,写的快读的慢
char buffer[64] = 0;
// 只要有数据就可以一直读
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // 如果read的返回值是0,意味着子进程关闭文件描述符
if (s == 0)
break;
else if (s > 0)
buffer[s] = 0;
printf("%s", buffer);
else
break;
return 0;
// int pipe(int pipefd[2]);
// pipefd[2]是一个输出型参数,可以通过这个参数读取到打开的两个fd
验证管道是由大小的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
int pipefd[2] = 0;
if (pipe(pipefd) != 0)
perror("pipe error!");
return 1;
// 让父进程进行读取,子进程进行写入
if (fork() == 0)
// 子进程
close(pipefd[0]);
const char *msg = "hello\\n";
int count = 0;
while (1)
// pipe只要有缓冲区就会一直写入
write(pipefd[1], "a", 1);
count++;
printf("count: %d\\n", count);
exit(0);
// 父进程
close(pipefd[1]);
while (1)
// 父进程不进行读取
return 0;
write写入65535个字节即64KB后就不再写了,说明管道是由大小的,会等待read来读,不继续写的本质就是等待对方来读
父进程只读取少量的字节,子进程并不会继续进行写入
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
int pipefd[2] = 0;
if (pipe(pipefd) != 0)
perror("pipe error!");
return 1;
// 让父进程进行读取,子进程进行写入
if (fork() == 0)
// 子进程
close(pipefd[0]);
const char *msg = "hello\\n";
int count = 0;
while (1)
// pipe只要有缓冲区就会一直写入
write(pipefd[1], "a", 1);
count++;
printf("count: %d\\n", count);
exit(0);
// 父进程
close(pipefd[1]);
while (1)
sleep(5);
char c[64] = 0;
read(pipefd[0], c, 64);
printf("%s\\n", c);
return 0;
原因:需要一次性读走4KB的数据,才会继续进行写入
- 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
- 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性
4种情况
-
读端不读或者读的慢,写端要等待读端
-
读端关闭,写端收到SIGPIPE信号直到终止
这本质是在浪费系统资源,操作系统会终止写入进程,OS给目标进程发送信号SIGPIPE
-
写端不写或者写的慢,读端要等待写端
-
写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾
2.2 命名管道
使用命名管道,双方通信只需要按照文件操作即可
因为命名管道是基于字节流的,所以实际上在信息传递的时候,是需要通信双方制定协议的
命令行上直接使用
创建管道文件
mkfifo fifo
这个管道是有名字的——fifo,使用它即可在命令行上进行简单的字符串通信
使用代码操作管道
server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define MY_FIFO "./fifo"
int main()
umask(0); // 将系统的umask清零,这样下面我们创建管道时的文件权限就不会受系统的umask影响
if (mkfifo(MY_FIFO, 0666) < 0)
perror("mkfifo");
return 1;
int fd = open(MY_FIFO, O_RDONLY);
if (fd < 0)
perror("open");
return 2;
while (1)
char buffer[64] = 0;
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
buffer[s] = 0;
if (strcmp(buffer, "show") == 0)
if (fork() == 0)
execl("/usr/bin/ls", "ls", "-l", NULL);
exit(1);
waitpid(-1, NULL, 0);
else if (strcmp(buffer, "run") == 0)
if (fork() == 0)
execl("/usr/bin/sl", "sl", NULL);
exit(1);
waitpid(-1, NULL, 0);
else
printf("client say#%s\\n", buffer);
else if (s == 0)
printf("client quit\\n");
break;
else
perror("read");
close(fd);
return 0;
client.c
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define MY_FIFO "./fifo"
int main()
int fd = open(MY_FIFO, O_WRONLY);
if (fd < 0)
perror("open");
return 1;
while (1)
printf("请输入#");
fflush(stdout);
char buffer[64] = 0;
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if(s > 0)
buffer[s - 1] = 0;
printf("%s\\n", buffer);
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
makefile
.PHONY:all
all:server client
server:server.c
gcc -o $@ $^
client:client.c
gcc -o $@ $^
.PHONYE:clean
clean:
rm -f server client fifo
命名管道的数据不会刷新到磁盘,这样可以提高效率,刷新到磁盘需要时间
为什么pipe叫做匿名管道,fifo叫做命名管道
- 匿名管道的文件没有名字,它是通过父子继承的方式来看到同一份资源,不需要名字来标识同一个资源
- fifo一定要有名字,为了保证不同的进程看到同一个文件,所以必须要有名字
3. System V
同样是基于文件的通信方式,在OS层面专门为进程间通信设计的一个方案
操作系统不相信任何用户,给用户提供功能的时候,采用系统调用
System V进程间通信,一定会存在专门用来通信的接口(system call)
在同一主机内的进程间通信方法:System V方案
进程间通信的本质:让不同的进程看到同一份资源,因而System V有三种方案
- 消息队列
- 共享内存
- 信号量
3.1 共享内存
如何让不同进程看到同一份资源
- 通过某种调用,在内存中创建一份内存空间
- 通过某种调用,让参与通信的多个进程“挂接”到这份新开辟的内存空间上
- 去关联
- 释放共享内存
操作系统中可能会存在多个进程,这样就会有很多不同的共享内存,因此OS就必须对这些共享内存进行管理,使不同的共享内存来进行进程间通信,管理的方式与之前类似:先描述,在组织;这就带来了一个问题,如何保证两个或多个进程看到的是同一份共享内存呢?共享内存一定会有标识唯一性的ID,方便让不同的进程识别同一个共享内存资源,而这个ID一定是存在于描述共享内存的数据结构中。这个唯一的标识符,就是用来进行进程间通信的,而这个ID也是需要由用户自己设定的,这样才能达到通信的目的
接口介绍
-
创建共享内存
-
控制共享内存
-
装载/卸载共享内存
① shmget
int shmget(key_t key, size_t size, int shmflg);
key_t key
:这个key就是会设置进内核中的关于shm在内核中的数据结构中
key如果由我们自己指定会很麻烦,且不能保证都记住,因此使用ftok
函数进行生成
const char *pathname
:自定义路径名,如"/tmp/Xxx"int proj_id
:自定义项目ID,如:0x6666
这样的话只要我们形成key的算法+原始数据是一样的,形成的key就是一样的,ID唯一
size_t size
:建议是4KB的整数倍
int shmflg
:标志位
IPC_CREAT
:如果单独使用这个或者标志位为0,则创建一个共享内存,如果创建的共享内存以及存在,则直接返回当前已经存在的共享内存IPC_EXCL
:单独使用没有意义,IPC_CREAT | IPC_EXCL
:如果不存在这个共享内存则直接创建,如果已经存在则报错- 还可以加上要创建的这个共享内存的权限:
IPC_CREAT | IPC_EXCL | 0666
,对应于perms
comm.h
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096
server.c
#include "comm.h"
int main()
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
perror("ftok");
return 1;
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
perror("shmget");
return 2;
printf("key: %#x, shmid: %d\\n", key, shmid);
return 0;
shmid为-1就表示创建失败了,即该共享内存已经存在
ipcs -m
:查看系统存在的共享内存
./server已经执行结束了,但是发现共享内存是一直存在的,并没有被释放,这表明System V的IPC资源生命周期是随内核的,只能程序员手动释放或者操作系统手动重启
ipcrm -m [shmid]
:释放共享内存
② shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
int shmid
:共享内存的shmid
int cmd
:对共享内存的操作
IPC_RMID
:释放该共享内存——shmctl(shmid, IPC_RMID, NULL)
IPC_STAT
:把shmid_ds结构中的数据设置为共享内存的当前关联值IPC_RMID
:在进程有1足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
struct shmid_ds *buf
:是一块结构体,描述的是shmid的数据结构
key vs shmid
- key:只是在系统层面上来进行唯一性标识的,不能用来管理共享内存
- shmid:是操作系统给用户返回的id,用来在用户层对共享内存进行管理
③ shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);
DESCRIPTION
shmat() attaches the System V shared memory segment identified by shmid to the address space of the calling process.
RETURN VALUE
On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate the cause of the error.
这里的地址也是虚拟地址,跟malloc出来的一样
④ shmdt
int shmdt(const void *shmaddr);
并不是释放共享内存,而是取消当前进程与共享内存之间的关系
DESCRIPTION
shmdt() detaches the shared memory segment located at the address specified by shmaddr from the address space of the calling process.
RETURN VALUE
On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
共享内存的整个生命周期
server.c
#include "comm.h"
int main()
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
perror("ftok");
return 1;
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
perror("shmget");
return 2;
printf("key: %#x, shmid: %d\\n", key, shmid);
sleep(5);
char *mem = (char*)shmat(shmid, NULL, 0);
printf("attach shm success\\n");
sleep(5);
// 在这里进行通信
shmdt(mem);
printf("detach shm success\\n");
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
printf("key: %#x, shmid: %d -> shm delete success\\n", key, shmid);
return 0;
使用共享内存进程间通信
server.c
#include "comm.h"
int main()
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 以上是关于Linux操作系统进程间通信的主要内容,如果未能解决你的问题,请参考以下文章