传统艺能😎
小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
1319365055
🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我
进程间通信🤔
进程间通信即 IPC
(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息,请务必牢记进程间通信的本质是 让 不 同 的 进 程 看 到 同 一 份 资 源 \color{red} {让不同的进程看到同一份资源} 让不同的进程看到同一份资源
进程间通信的目的有 4
个:
数据传输: 一个进程需要将它的数据发送给另一个进程
资源共享: 多个进程之间共享同样的资源
通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程
进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
而进程间通信实际上实现是比较困难的,因为进程具有独立性,而且代码逻辑上可以公有和私有,比如父子进程,既然自己没办法做到那就借助外力:第三方资源
我们此时就需要一个第三方资源来提供一段公有区域来 “交流” 两方进程,并且支持读写操作。
通信方式🤔
管 道 : \color{red} {管道:} 管道:
匿名管道
命名管道
S y s t e m V I P C : \color{red} {System V IPC:} SystemVIPC:
System V 消息队列
System V 共享内存
System V 信号量
P O S I X I P C : \color{red} {POSIX IPC:} POSIXIPC:
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道🤔
管道是 Unix 中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个 “管道”。比如统计我们当前使用云服务器上的登录用户个数:
who | wc -l
who 用于查看当前服务器的登录用户(一行一个小朋友),wc -l 用于统计当前的行数
这里 | 就是一个管道标志,who 和 wc 是两个命令,who 将数据通过标准输出输出到管道中,wc 再通过标准输入流读取数据:
匿名管道🤔
匿名管道用于进程间通信,但仅限于本地父子进程之间的通信
记得我们进程通信的本质是让不同进程看到同一份资源,于是匿名管道的原理就是先让父子进程看到一个已经打开的文件资源,然后就可以进行读写操作完成通信。
这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
管道虽然用的是文件的方案,但操作系统一定不会把通信的数据刷新到磁盘中,因为这样存在 IO 参与会降低效率,而且也没必要。这种文件是不会把数据写到磁盘当中的文件
,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在
pipe🤔
pipe 函数是用于创建匿名管道:
int pipe(int pipefd[2]);
pipe 调用成功时返回 0,调用失败时返回 -1。pipe 的参数是一个输出型参数,数组 pipefd 用于返回两个指向管道读端和写端的文件描述符:
匿名管道使用🤔
创建匿名管道实现父子进程间通信时需要pipe函数和fork函数搭配使用
:
1、父进程调用pipe函数创建管道:
2、父进程创建子进程:
3、父进程关闭写端,子进程关闭读端:
管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。
站在文件描述符
角度再来看看这三个步骤:
1、父进程调用pipe函数创建管道:
2、父进程创建子进程:
3、父进程关闭写端,子进程关闭读端:
比如我们向子进程向匿名管道当中写入10行数据,父进程从匿名管道当中将数据读出:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
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);
}
//父进程关闭写端
close(fd[1]);
//父进程从管道读取数据
char buff[64];
while (1){
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0){
buff[s] = '\0';
printf("child send to father:%s\n", buff);
}
else if (s == 0){
printf("read file end\n");//父进程读取完毕
break;
}
else{
printf("read error\n");
break;
}
}
close(fd[0]);
waitpid(id, NULL, 0);
return 0;
}
效果如下:
读写规则🤔
pipe2 函数与 pipe 函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
flags
参数用于设置选项:
当管道空的时候:
O_NONBLOCK disable:read 调用阻塞,即进程暂停执行,一直等到有数据来为止
O_NONBLOCK enable:read 调用返回 -1,errno 值为EAGAIN
当管道满的时候:
O_NONBLOCK disable:write 调用阻塞,直到有进程来读数据 O_NONBLOCK enable:write 调用返回-1,errno 值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则 read 返回 0;如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE
,进而可能导致 write 进程退出。
当要写入的数据量不大于 PIPE_BUF 时,Linux 会保证写入的原子性
。当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性
管道特点🤔
同步与互斥😋
首先管道内部自带同步与互斥机制,管道一次只允许一个进程使用的资源,称为临界资源
。在同一时刻也只允许一个进程对其进行读写操作,因此管道也就是一种临界资源
临界资源是需要被保护的,若不对管道进行保护,就可能出现同一时刻多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写或读取到数据不一样等问题,为了避免这些问题,内核会对管道操作进行同步与互斥
同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行,比如,A任务的运行依赖于B任务产生的数据
互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
本质上同步是更为复杂的互斥,而互斥是一种特殊的同步。互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系
管道生命周期😋
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件才会被释放掉,所以说管道的生命周期伴随进程
流式服务😋
流式服务即对于进程 A 写入管道当中的数据,进程 B 每次从管道读取数据的多少是任意的,与之相对应的是数据报服务:
流式服务: 数据没有明确的分割,不分一定的报文段
数据报服务: 数据有明确的分割,拿数据按报文段拿
半双工通信😋
数据通信中,数据在线路上的传送方式可以分为三种:
单工通信:单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
半双工通信:半双工数据传输是单向的,但通信双方中,没有规定谁是发送端谁是接收端
全双工通信:全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输
4种特殊情况🤔
-
写端进程不写,读端进程一直读,此时会因为没有数据可读,读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒
-
读端进程不读,写端进程一直写,写端进程写满后会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒
-
写端进程数据写完后将写端关闭,那么读端将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起
-
读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会主动将写端进程杀掉
前两种情况就能够很好的说明管道是自带同步与互斥机制的。我们再来看看四种情况子进程退出时对应收到的不同的信号:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
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); //打印子进程收到的信号(低7位)
return 0;
}
子进程退出时收到的是13号信号:
通过kill -l
命令可以查看 13 对应的具体信号:
kill -l
此时将子进程终止的的就是SIGPIPE
信号
管道大小🤔
管道的容量是有限的,所以管道的最大容量是多少呢?此时我就可以使用 man
命令进行查看:
根据内容在 2.6.11 之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11 往后,管道的最大容量是 65536 字节。所以我们 uname -r
查看当前 Linux 版本
uname -r
可以看出当前是Linux 2.6.11之后的版本,所以最大容量是 65536 字节
我们还可以使用ulimit -a
命令,查看当前资源限制的设定:
所以管道的最大容量是 512 × 8 = 4096 字节
命名管道🤔
匿名管道只能用于具有共同祖先的进程,即一个管道由一个进程创建,然后该进程调用 fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程通信,就要使用命名管道。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了
注意:
普通文件是很难通信的,即便做到通信也无法解决一些安全问题。命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为 0,因为管道不会将通信数据刷新到磁盘!
命名管道创建😋
使用mkfifo
命令创建一个命名管道:
mkfifo fifo
这里创建出来的文件类型为 p,代表该文件是命名管道文件
类型。
我们在进程 A 中每秒向命名管道写入一个字符串,在进程 B 中用cat
命令从命名管道当中进行读取。现象是当进程 A 启动后,进程 B 会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据通信
这里可以很好的验证当管道的读端进程退出后,写端进程会被操作系统杀掉:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器 bash 执行的,所以此时 bash 就会被操作系统杀掉,服务器也就退出了
mkfifo
函数的函数原型如下:
int mkfifo(const