进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
每个进程有各自不同的进程地址空间,互相独立,一般不能互相访问,一个进程的变量在另一个进程中看不到,但是内核空间是进程间共享的,进程间通信需要通过内核,内核中开辟空间,不同进程在该缓冲区内写入数据、读出数据,实现通信。
一般的进程通信分为:管道、消息队列、共享内存、信号量、信号和Socket。
管道(英语:Pipeline)是一系列将标准输入输出链接起来的进程,其中每一个进程的输出被直接作为下一个进程的输入。
每一个链接都由匿名管道实现[来源请求]。管道中的组成元素也被称作过滤程序。
一般使用过linux系统的同学都使用过管道符同时执行多个程序,例如:
ls -l | less
ls 和 less 分别是两个独立的进程。shell 会将 ls 的输出结果作为 less 的输入结果,然后再由 less 把处理结果投放到终端上。
上面命令行里的「|
」竖线就是一个管道,它的功能是将前一个命令(ls -l
)的输出,作为后一个命令(less
)的输入。
可以看出管道传输数据是单向的,和实际的管道很像,如果想相互通信,我们需要创建两个管道才行。
使用C语言在UNIX中使用pipe系统调用时,这个函数会让系统构建一个匿名管道,这样在进程中就打开了两个新的,打开的文件描述符:一个只读端和一个只写端。
管道的两端是两个普通的,匿名的文件描述符,这就让其他进程无法连接该管道。
为了避免死锁并利用进程的并行运行的好处,有一个或多个管道的UNIX进程通常会调用fork产生新进程。并且每个子进程在开始读或写管道之前都会关掉不会用到的管道端。
或者进程会产生一个子线程并使用管道来让线程进行数据交换。
#include
#include
#include
#include
#include
int
main(int argc, char *argv[])
{
int pipefd[2];
pid_t cpid;
char buf;
if (argc != 2) {
fprintf(stderr, "Usage: %s \n" , argv[0]);
exit(EXIT_FAILURE);
}
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* Child reads from pipe */
close(pipefd[1]); /* Close unused write end */
printf("\nchild receives data from father: \n");
while (read(pipefd[0], &buf, 1) > 0)
write(STDOUT_FILENO, &buf, 1);
printf("%c", buf);
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);
} else { /* Parent writes argv[1] to pipe */
close(pipefd[0]); /* Close unused read end */
write(pipefd[1], argv[1], strlen(argv[1]));
printf("\nfather wirte data: %s\n", argv[1]);
close(pipefd[1]); /* Reader will see EOF */
wait(NULL); /* Wait for child */
exit(EXIT_SUCCESS);
}
}
形象化思路就是:
所以父进程写入管道的时候,子线程就读出数据,不能两个进程同时写入,会造成数据冲突。因为是单向的,所以要么父进程写入,子进程读出,要么子进程写入,父进程读出。
运行程序:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/multithreading$ ./randy Randy
father wirte data: Randy
child receives data from father:
Randy
在 shell 里面执行 A | B
命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系。他们是并列的2个进程,共用1个管道
具名管道可以通过调用mkfifo或
mknod来构建,当被调用时表现为输入或输出的文件。
创建具名管道接口:
int mkfifo(const char *pathname, mode_t mode);
int mknod (const char *__path, __mode_t __mode, __dev_t __dev)
// 返回值:成功返回0;失败返回-1
命名管道的特性:
这样可以允许建立多个管道,并且将其同标准错误重定向或tee
结合起来使用更为有效。 实现代码:
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
char filename[] = "randy_fifo";
if (!mkfifo(filename, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)) {
pid_t pid = fork();
if (pid == 0) { // child
int fd = open(filename, O_WRONLY);
if (fd < 0)
perror("child open()");
else {
printf("\nchild write data : %s\n", argv[1]);
if (strlen(argv[1]) != write(fd, argv[1], strlen(argv[1])))
perror("child write error");
else
close(fd);
}
} else if (pid > 0) { // father
int fd = open(filename, O_RDONLY);
if (fd < 0)
perror("father open()");
else {
char buffer[200];
int readed = read(fd, buffer, 199);
printf("\father read data from child: %s\n", buffer);
close(fd);
buffer[readed] = '\0';
printf("%s\n", buffer);
}
} else {
perror("fork()");
}
} else {
perror("mkfifo() error:");
}
return 0;
}
执行结果:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/multithreading$ ./randy RandyJeff
child write data : RandyJeff
ather read data from child: RandyJeff
RandyJeff
linux系统一切皆是文件的思路,发现具名管道也是文件,匿名管道可以看成是内存中的文件:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/multithreading/pipe$ ll randy.fifo
prw-rw-r-- 1 qiancj qiancj 0 8月 18 10:12 randy.fifo|
fifo_read.c
//fifo_read.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
//创建管道文件
int ret = mkfifo("./randy.fifo",0664 );
//管道文件不是因为存在而创建失败
if (ret < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
//以只读方式获取管道文件的操作句柄
int fd = open("./randy.fifo", O_RDONLY);
if (fd < 0)
{
perror("open fifo error");
return -1;
}
printf("open fifo success\n");
int i = 0;
//一直读数据
while(1)
{
char buf[1024] = {0};
//将从管道读取的文件写到buf中
int ret = read(fd, buf, 1023);
++i;
if (ret < 0)
{
perror("read error");
return -1;
}
//所有写端都关闭
else if (ret == 0)
{
perror("all write closed");
return -1;
}
//打印所读到的数据
printf("read buf:[%s] %d\n",buf, i);
}
close(fd);
return 0;
}
fifo_write.c
//fifo_write.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int ret = mkfifo("./randy.fifo", 0664);
if (ret < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
//以只写的方式打开管道文件
int fd = open("./randy.fifo", O_WRONLY);
if (fd < 0)
{
perror("open fifo error");
return -1;
}
printf("open fifo success\n");
int i = 0;
//一直写
while(1)
{
char buf[1024] = {0};
//将字符串内容写到buf中,并记录写的次数
sprintf(buf, "Hello Randy", i++);
//将buf中的内容写到管道中
write(fd, buf, strlen(buf));
printf("write data success\n");
sleep(1);
}
close(fd);
return 0;
}
执行写进程,读进程就能够读到数据了:
停掉写进程,只读进程中的read接口会返回0,通过该值控制进程退出,读进程也会停止
对于匿名管道,通信范围是父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。
另外,对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
在 Linux 系统中,我们可以使用下图的方式来查看各级 CPU Cache 的大小,比如我这手上这台服务器,离 CPU 核心最近的 L1 Cache 是 32KB,其次是 L2 Cache 是 256KB,最大的 L3 Cache 则是 3MB。
(base) qiancj@qiancj-HP-ZBook-G8:~$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
48K
(base) qiancj@qiancj-HP-ZBook-G8:~$ cat /sys/devices/system/cpu/cpu0/cache/index1/size
32K
(base) qiancj@qiancj-HP-ZBook-G8:~$ cat /sys/devices/system/cpu/cpu0/cache/index2/size
1280K
(base) qiancj@qiancj-HP-ZBook-G8:~$ cat /sys/devices/system/cpu/cpu0/cache/index3/size
24576K
https://mp.weixin.qq.com/s?__biz=MzUxMjEyNDgyNw==&mid=2247514907&idx=1&sn=0989d590cd479eee5b9d86cb7356ce1a&chksm=f96bc9efce1c40f943823ee8354573134b84a3cc842e96fb4412fe985f13ece3ec5c7c816fb7&mpshare=1&scene=1&srcid=0816z8wo8qDR630oSFqCITSZ&sharer_sharetime=1692179392154&sharer_shareid=9282c33bd664bcf868293e52a214b059#rd
https://cloud.tencent.com/developer/article/1736932
https://huaweicloud.csdn.net/63564439d3efff3090b5cdec.html#FIFO_108
https://zh.wikipedia.org/wiki/%E7%AE%A1%E9%81%93_(Unix)
https://xiaolincoding.com/os/4_process/process_commu.html#%E7%AE%A1%E9%81%93