操作系统为用户提供的几种进程间通信方式,让进程之间能够进行通信。
进程间通信----------多个进程之间数据的交互
Q:为什么不能直接进行数据交互?需要使用系统提供的接口来实现进程间通信?
A:进程间具有独立性,每个进程都有自己的虚拟地址空间,访问数据时是通过自己的虚拟地址访问的,一个进程将自己的某个变量的空间地址(虚拟地址)交给另一个进程,另一个进程是无法通过页表进行访问到的。
操作系统提供进程间通信方式其实就是给进程间提供一个空间的交叉点,让多个进程都可以访问的到,从而实现进程通信
操作系统提供进程间的通信方式(根据不同场景):
管道:适用于数据传输场景
共享内存:适用于数据共享场景
消息队列:适用于数据传输场景
信号量:适用于进程的协同控制(实现进程间的同步与互斥)
管道的特性:半双工通信(可以选择方向的单向通信—同一时间不能同时既发送又接收)
(1)单工通信:
单向通信,假设两端A,B,单工就是只能 A->B;
(2)双工通信:
双向通信,A->B & B->A 可以同时进行
(3)半双工通信:
可以选择方向的单向通信,要么 A->B,要么 B->A,俩个方向不能同时进行
管道的本质:
操作系统给进程之间提供的通信方式,本质是提供了一个空间的交叉点,都能访问
在程序中,管道其实就是内核中的一块缓冲区(一块内存)------- 由操作系统进行管理;多个进程通过访问同一块缓冲区来实现数据传输
但是在 Linux 下一切皆文件,故将管道当作文件来处理,不是传统意义上的磁盘文件,本质上是内存
对于空调电视机来说,句柄就是遥控器。
一个进程通过系统调用接口创建完管道后,这个系统调用接口就给进程返回一个管道的操作句柄。
匿名管道没有标识符,无法被其他进程找到,所以无法进行通信,因此只能通过创建子进程的方式来进行,子进程复制了父进程,也就复制了父进程所拥有的所有操作句柄,通过这个句柄它可以访问这个管道。
没有名字(标识符)的管道;将无法被其他进程找到
特性:只能用于具有亲缘关系的进程间通信
匿名管道类似于家族财产,只有同一个家族的人才有操作句柄能操作。
子进程复制父进程的资源,同时复制了父进程的管道信息(管道的操作句柄)
操作句柄:
文件描述符(因为管道被当作文件来进行操作)
通过文件描述符这个下标,就能够找到管道的描述信息,进而对文件进行操作
发送数据就是向管道中写入数据;接收数据就是从管道中读取数据。
程序替换会初始化虚拟地址空间,但是不会初始化文件的描述符表,因此重定向了子进程的标准输出后,进行程序替换依然有效;
程序替换之后,新程序初始化了虚拟地址空间,意味着以前保持了文件描述符的这个变量没了,因此创建管道后,将标准输入输出的描述符重定向到管道,这时候操作标准输入输出就是操作管道
在创建子进程之前先创建匿名管道,以便于子进程能够复制到父进程中的管道描述符信息
int pipe(int pipefd[2]);
功能:创建一个管道,并通过参数返回管道的俩个操作句柄
参数:pipefd - 两个整形元素的数组,内部创建管道会将描述符存储在数组中
(1) pipefd[0]: 用于从管道中读取数据
(2) pipefd[1] : 用于向管道中写入数据
但是正常情况下 读取 与 写入 不同时使用
返回值:创建失败返回 -1,成功返回 0
注意:创建匿名管道一定要在创建子进程之前,确保子进程能够复制父进程中的管道句柄信息
创建管道:
1 #include
2 #include
3 #include
4 #include
5 #include
6
7 int main()
8 {
9 int pipefd[2];
10 int ret=pipe(pipefd); //创建一个管道
11
12 if(ret<0){ //管道创建失败
13 perror("pipe error");
14 return -1;
15 }
16 //pipefd[0]读取 pipefd[1]写入
17
18 int pid1=fork();
19 if(pid1==0){
20 //创建一个子进程1
21 //pipefd[0]读取 pipefd[1]写入
22 //让子进程1写入数据,子进程2来读取
23
W> 24 char *data="好好学习Linux,否则没有工作!\n";
25 write(pipefd[1],data,strlen(data)); //向管道中写入数据
26 exit(0);
27 }
28
29
30 int pid2=fork();
31 if(pid2==0){
32 //子进程2
33 //读取数据
34 int buf[1024];
35 read(pipefd[0],buf,1023); //从管道中读取数据
W> 36 printf("收到了老大的警告: %s\n",buf);
37 exit(0);
38 }
39
40 wait(NULL); //等待任意子进程退出
41 wait(NULL); //等待任意子进程退出,只有当所有子进程都退出之后,父进程才能退出
42
43 return 0;
44 }
子进程谁先运行不一定,如何保证子进程2一定能读取到子进程1的警告?
让 子进程1 :sleep(3) ,在运行该代码会发现,运行之后程序会等待 3s ,然后输出读取的数据在退出-------read 当管道中间没有数据会一直等待,直到管道中存在数据时在进行读取打印。
管道特性
(1)管道中若没有数据,则 read 读取数据时会阻塞,直到有数据了,读取数据后才能返回;
(2)若管道中数据满了,则 write 继续向管道中写入数据会阻塞,直到管道中有剩余空间才行(有数据被读取出去)
核心代码:
//让子进程1循环写入数据
//同时子进程2循环读取数据
//则运行代码会形成交替作用,空间写满--进行读取,读取之后空间不满----在开始写入
//子进程1循环写入
19 if(pid1==0){
20 //创建一个子进程1
21 //pipefd[0]读取 pipefd[1]写入
22 //让子进程1写入数据,子进程2来读取
23 int total=0;
24 while(1){
W> 25 char *data="好好学习Linux,否则没有工作!\n";
26 int ret = write(pipefd[1],data,strlen(data));
27 total+=ret;
28 printf("已经写入了 %d 个字节的数据\n",total);
29 }
30 exit(0);
31 }
//子进程2循环读取
while(1){
40 sleep(1);
41 read(pipefd[0],buf,1023);
W> 42 printf("收到了老大的警告: %s\n",buf);
43 }
(3)管道的所有 读端 被关闭,则继续向管道写入数据会导致进程崩溃退出
7 int main()
8 {
9 int pipefd[2];
10 int ret=pipe(pipefd); //创建一个管道
11
12 if(ret<0){
13 perror("pipe error");
14 return -1;
15 }
16
17
18 int pid1=fork();
19 if(pid1==0){
21 close(pipefd[0]); //关闭读端
27 printf("write start!\n");
28
29 while(1){ //循环写入数据
W> 30 char *data="好好学习Linux,否则没有工作!\n";
31 int ret = write(pipefd[1],data,strlen(data));
32
33 if(ret==0){
34 //写入到的数据为 0 ,表示当前没有数据被读取了,故没必要在进行写入数> 据了
35 printf("write over!\n");
36 }
37 }
38 exit(0);
39 }
40
41
42 int pid2=fork();
43 if(pid2==0){
44 //子进程2
45 //读取数据
46
47 close(pipefd[0]); //关闭读端
48 exit(0); //退出子进程2
49
50 int buf[1024];
51 while(1){
52 sleep(1);
53 read(pipefd[0],buf,1023);
W> 54 printf("收到了老大的警告: %s\n",buf);
55 }
56 // exit(0);
57 }
58
59 //从父进程中关闭读端
60
61 close(pipefd[0]); //关闭读端
62
63 wait(NULL); //等待任意子进程退出
64 wait(NULL);
65
66 return 0;
67 }
同时要在父进程中也应该进行关闭读端
(4)管道的所有 写端 被关闭,则 read 读取完管道中数据,之后继续向管道中读取数据,将不再阻塞,而是返回 0
close(pipefd[1]);
//代码类似于上一段代码
父进程中也应该关闭写端
管道若所有能够写入数据的描述符被关闭了,意味着管道中不可能再有数据进来,因此也没有必要在进行读取数据了;
当 read 从管道中读取数据时,返回 0 了,意味着这个管道已经不可能在读取到数据了,因此可以通过 read 的返回值,来决定什么时候停止从管道中读取数据。
思考:ps -ef | grep pipe 怎么实现的?
ps -ef :将进程信息写入到标准输出文件
grep pipe1:捕捉标准输入的数据,进程过滤输出
实现:
(1) 父进程首先创建管道;
(2)创建子进程 1,将子进程 1 的标准输出重定向到管道的写入端,程序替换为 ps 进程;
ps 进程向标准输出写入数据也就是向管道中写入数据;
(3)创建子进程 2 ,将子进程 2 的标准输入重定向到管道的读取端,程序替换为 grep 程序;
这时候 grep 程序从标准输入读取数据也就是从管道中读取
1 #include
2 #include
3 #include
4 #include
5
6 int main()
7 {
8 int pipefd[2]={0};
9 //创建管道
10 int ret=pipe(pipefd);
11 if(ret<0){
12 perror("pipe error");
13 return -1;
14 }
15
16 pid_t ps_pid=fork();
17 if(ps_pid==0){
18 close(pipefd[0]); //只是写入,关闭读端
19 dup2(pipefd[1],1); //将标准输出重定向到管道写入端,则此时向标准输出端写入 数据==向管道中写入数据
20 execlp("ps","ps","-ef",NULL); //程序替换,替换字符以 NULL 结尾
21 exit(-1);
22 }
23
24 pid_t grep_pid=fork();
25 if(grep_pid==0){
26 close(pipefd[1]); //读端,关闭写端
27 dup2(pipefd[0],0); //将标准输入重定向到管道读取端,则此时从标准输入读取数据 == 从管道读取数据
28 execlp("grep","grep","pipe",NULL); //程序替换,字符以 NULL 结尾
29 exit(-1);
30 }
31
32
33 //父进程中关闭读写端
34 close(pipefd[0]);
35 close(pipefd[1]);
36
37 waitpid(ps_pid,NULL,0); //阻塞等待指定子进程 ps_pid 退出
38 waitpid(grep_pid,NULL,0); //阻塞等待指定子进程 grep_pid 退出,0-阻塞,WNOHANG-非阻塞等待
// waitpid (-1,&status,0); // -1 等待任意子进程退出,0-阻塞等待,status 用来存放退出子进程的返回值;若第一个参数 >0 表示等待指定的子进程退出
39
40 return 0;
41 }
有名字(标识符)的管道,可以被其他进程找到
特性:可以用于同一主机上任意进程通信
命名管道的通信原理------一个进程创建了一个命名管道的名字
多个进程通过相同的管道名字,打开同一个管道,访问同一块缓冲区
命名管道的名字:是一个可见于文件系统的管道文件
命名管道文件,虽然是个文件,但实际上只是一个名字,能够让多个进程通过打开同一个命名管道文件,进而获取到同一个管道缓冲区的描述信息(操作句柄),进而访问同一块缓冲区进行通信。
实质上的通信依然是通过内核的缓冲区完成的,而不是这个文件。
创建一个命名管道:实际上是一个管道文件
mkfifo 命名管道的管道名
管道文件只是一个标识符,是一个名字,只有在打开管道文件,进行进程间通信时才会开辟缓冲区
int mkfifo(char* pathname,mode_t mode);
pathname:管道文件名称
mode:管道文件的访问权限
返回值:成功返回 0,失败返回 -1
在文件内部创建命名管道:
若当前要创建的命名管道已经存在时,再次进行创建会报出一条提示:file exist 文件已经存在,则可以使用全局变量 errno 来进行容错处理
#include
errno!=EEXIST
命名管道独有特性
若以只写方式打开管道文件,则会阻塞,直到管道被任意进程以读方式打开
若以只读方式打开管道,则会阻塞,直到管道被任意进程以写方式打开
因为一个管道若不构成同时读写,就没有必要开辟缓冲区
//write_fifo.c
#include
#include
#include
#include
#include
#include
#include //当要创建的命名文件已经存在时
int main()
{
//向命名管道中写入数据
umask(0); //设置当前进程的文件权限掩码为 0
//创建一个命名管道,管道名 test.fifo
int ret=mkfifo("./test.fifo",0664);
if(ret<0 && errno!=EEXIST){ //errno 是一个全局变量,每个系统调用接口使用返回之前都会重置自己的错误编号
perror("mkfifo error");
return -1;
}
//打开管道并向管道写入数据
int fp=open("./test.fifo",O_WRONLY); //只写方式打开
if(fp<0){
perror("open error");
return -1;
}
while(1){
printf("小明:");
fflush(stdout);
char buf[1024]={0};
scanf("%s",buf); //从键盘写入字符信息
//向管道写入数据
int ret=write(fp,buf,strlen(buf));
if(ret<0){
perror("write error");
close(fp);
return -1;
}
}
close(fp);
return 0;
}
//read_fifo.c
#include
#include
#include
#include
#include
#include
#include //当要创建的命名文件已经存在时
int main()
{
umask(0); //设置当前进程的文件权限掩码为 0
//创建一个命名管道,管道名 test.fifo
int ret=mkfifo("./test.fifo",0664);
if(ret<0 && errno!=EEXIST){ //errno 是一个全局变量,每个系统调用接口使用返回之前都会重置自己的错误编号
perror("mkfifo error");
return -1;
}
//打开管道并向管道读取数据
int fp=open("./test.fifo",O_RDONLY); //只读打开
if(fp<0){
perror("open error");
return -1;
}
while(1){
char buf[1024]={0};
//从管道读取数据
int ret=read(fp,buf,1023);
if(ret<0){
perror("read error");
close(fp);
return -1;
}
else if(ret==0){
printf("所有写端被关闭!\n");
close(fp);
return -1;
}
printf("小明:%s\n",buf);
}
close(fp);
return 0;
}
makefile 中名字替换
(1)半双工通信
(2)管道生命周期随进程
不人为关闭的情况,所有打开管道的进程退出后,管道就会被释放
(3)提供字节流传输服务
数据先进先出,按序到达,不会丢失数据,面向连接
所有读端被关闭,继续写会异常;所有写端被关闭,继续读,读取完数据后不再阻塞,而是返回 0
(4)自带互斥与同步
互斥:通过同一时间的唯一访问,来确保访问的安全性
管道的读写操作在不超过 PIPE_BUF (4096字节)大小时,保证原子性(不可分割特性------原子操作:一个操作不会被打断)
同步:通过对资源的访问进行一些条件限制,让进程对资源的访问更加合理
最快的进程间通信方式
作用: 用于多个进程之间的数据共享
原理: 开辟一块物理内存,然后多个进程将这块内存映射到自己的虚拟地址空间,通过虚拟地址空间直接访问物理内存中的数据。
共享内存:
多进程将同一块内存映射到自己的地址空间,通过虚拟地址直接访问内存数据
其他的通信方式:
例如管道:进程A将数据拷贝到管道缓冲区,进程B从缓冲区将数据拷贝到自己的空间
共享内存的操作相较于其他进程间通信方式来说少了两次数据拷贝过程,因此共享内存是最快的进程间通信方式。
#include
头文件
(1)创建一块内存 shmget
int shmget(key_t key,size_t size,int shmflg);
(1) key:共享内存的标识符(名字)
(2) size:共享内存大小,会四舍五入到 PAGE_SIZE 的整数倍--------一般为 4096 字节
(3) shmflg:
使用 : IPC_CREAT | IPC_EXCL | 0664
IPC_CREAT :若共享内存不存在则创建打开,若存在则直接打开
IPC_EXCL :与 IPC_CREAT 搭配使用,共享内存不存在则创建打开,若存在则报错返回
mode_flags :共享内存的访问权限------0664
(4)返回值:成功返回一个操作句柄(非负整数),失败返回 -1
(2)将内存映射到自己的虚拟地址空间 shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);
(1)shmid:shmget 打开共享内存时返回的操作句柄
(2)shmaddr:映射首地址,通常置为 NULL,让操作系统进行分配
(3)shmflg:默认为 0 表示可读可写,SHM_RDONLY 表示只读(前提是必须具备对共享内存的操作权限)
(4)返回值:成功返回映射首地址,失败返回(void*)-1 -------> 因为函数返回值为 void* 类型
(3)内存操作
获取到首地址后,就可以通过首地址访问内存中数据,已经可以修改内存中数据
(4)解除映射关系 shmdt
int shmdt(const void *shmaddr);
shmaddr :映射首地址,也就是 shmat 返回值
(5)删除共享内存 shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
(1)shmid:是 shmget 返回的操作句柄
(2)cmd:对共享内存要进行的操作
IPC_RMID:标记一个共享内存段需要被删除
(3)buf:用于设置或获取共享内存信息,当 cmd 是 IPC_RMID 时被忽略
Q:多个进程访问同一个共享内存,突然有一个进程要删除共享内存,如何避免其他进程不会出现错误??
共享内存是有个当前的映射连接计数(表示有多少个进程正在访问)
所以这里的 RMID 就叫做标记删除,并不是真正删除,被标记的共享内存将不再接收新的映射,而是等当前映射连接计数为 0 时在实际删除,删除由系统来完成,进程所做的删除操作,其实只是标记一下。
代码练习:
运行结果:
练习:
int printf(const char *format, …); //直接标准输出
int fprintf(FILE *stream, const char *format, …); //数据写入文件
int sprintf(char *str, const char *format, …); //将要输出数据格式化后写入 str 字符串
int snprintf(char *str, size_t size, const char *format, …);
ipcrm 指令用于删除某些 IPC 资源
包含消息队列(message queue)、共享内存(shared memory)、信号量(semaphore),同时将与 IPC 对象关联的数据一并删除
ipcrm 选项说明
ipcrm [options]
ipcrm {shm|msg|sem} id…
-a, --all [shm | msg | sem]
删除所有 IPC 资源。当给定选项参数 shm、msg 或 sem,则只删除指定类型的 IPC 资源。
注意:慎用该选项,否则可能会导致某些程序出于不确定状态
-M, --shmem-key SHMKEY
当没有进程与共享内存段绑定时,通过 SHMKEY 删除共享内存段
-m, --shmem-id SHMID
当没有进程与共享内存段绑定时,通过 SHMID 删除共享内存段
-Q, --queue-key MSGKEY
通过 MSGKEY 删除消息队列
-q, --queue-id MSGID
通过 MSGID 删除消息队列
-S, --semaphore-key SEMKEY
通过 SEMKEY 删除信号量
-s, --semaphore-id SEMID
通过 SEMID 删除信号量
-h, --help
显示帮助信息并退出
-V, --version
显示版本信息并退出
-v, --verbose
以冗余模式执行 ipcrm,输出 rpcrm 正在做什么
ipcs :查看所有进程间通信方式状态
ipcs -m :显示进程间通信间资源–共享内存
s—>show;m---->memory 共享内存
ipcrm -m shmid 删除shmid共享内存
nattch = 2 ,表示当前映射连接数为 2 ---------- shmwrite 、shmread
(1)最快的进程间通信方式(少了两次数据拷贝----从用户态拷贝到内存,从内存拷贝到用户态)
(2)生命周期不会随进程退出而释放,随内核
(3)共享内存的访问操作存在安全问题--------竞争访问出现数据二义性
功能:实现进程间数据传输
本质:内核中的一个优先级队列
实现:多个进程通过访问同一个消息队列,以添加数据节点和获取数据节点实现通信
添加的数据节点------type / data
type 的作用:身份标识、优先级
特性:
(1)生命周期随内核
(2)自带同步与互斥
(3)传输是一种数据块的传输
作用:实现进程间同步与互斥
本质:是一个计数器 + PCB 等待队列
同步实现:
P 操作:对计数器进行 -1 操作,判断计数器是否大于等于 0,正确则返回,失败则阻塞
V 操作:对计数器进行 +1 操作,若计数器小于等于 0 ,则唤醒一个等待进程
通过计数器对共享资源进行计算,在获取资源之前进行 P 操作 ,计数满足访问条件则访问,若不满足则阻塞
当产生一个资源,则进程 V 操作,唤醒阻塞的进程
互斥实现:
初始化计数器为 1 ,标识资源只有一个
访问资源之前进行 P 操作(-1),访问资源完毕之后进行 V 操作( +1 唤醒等待)。
ps:
有任何问题欢迎评论留言呀~~
关于进程控制方面的知识(进程等待)----- 可以参考本人之前的博客 “linux 系统编程(中)”