Linux系统编程——进程间通信
在学习Linux系统编程总结了笔记,并分享出来。有问题请及时联系博主:Alliswell_WP,转载请注明出处。
09-linux-day06(进程间通信)
目录:
一、学习目标
二、进程通信——管道
1、管道的概念
2、管道通信举例
3、父子进程实现ps、grep命令
4、ps、grep命令实现问题解决
5、管道的读写行为
6、管道大小和优劣
三、进程通信——FIFO
1、fifo实现通信写端
2、fifo使用注意事项
四、进程通信——mmap
1、mmap映射开始
2、mmap注意事项
3、mmap实现父子进程通信
4、匿名映射
5、mmap实现无血缘关系进程通信
6、mmap(MAP_SHSRED)再次说明
五、进程通信——信号
1、信号的概念
一、学习目标
1、熟练使用pipe进行父子进程间通信
2、熟练使用pipe进行兄弟进程间通信
3、熟练使用fifo进行无血缘关系的进程间通信
4、熟练掌握mmap函数的使用
5、掌握mmap创建匿名映射区的方法
6、使用mmap进行有血缘关系的进程间通信
7、使用mmap进行无血缘关系的进程间通信
二、进程通信——管道
》IPC方法:进程间通信,通过内核提供的缓冲区进行数据交换的机制。
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
1)pipe 管道 (只能有血缘关系的,使用最简单)
2)fifo 信号 (开销最小)
3)mmap 共享映射区 (无血缘关系) 速度最快
4)本地socket 本地套接字 (最稳定)
5)信号(携带信息量最小)
6)共享内存
7)消息队列
1、管道的概念
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
1. 其本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
① 数据一旦被读走,便不在管道中存在,不可反复读取。
②由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
③ 只能在有公共祖先的进程间使用管道。
常见的通信方式有,单工通信(广播)、半双工通信(对讲机)、全双工通信(打电话)。
2、管道通信举例
》创建管道
man pipe
int pipe(int pipefd[2]);
pipefd读写文件描述符,0代表读,1代表写
返回值:失败返回-1,成功返回0
函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
练习:
>touch pipe.c
>vi pipe.c
1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 int fd[2];//声明数组 7 pipe(fd);//创建管道 8 pid_t pid = fork();//创建子进程 9 10 if(pid == 0){ 11 //son 12 sleep(3); 13 write(fd[1],"hello",5); 14 } 15 else if(pid > 0){ 16 //parent 17 char buf[12]={0}; 18 int ret = read(fd[0],buf,sizeof(buf));//read会等待write写 19 if(ret > 0){ 20 write(STDOUT_FILENO,buf,ret);//相当于printf,打印 21 } 22 } 23 return 0; 24 }
>make
>./pipe
(父进程会等待,直到管道中有数据。read函数特性,读设备(如:管道),read默认是阻塞的,只要对方打开了管道,就一直(等待)阻塞,直到有水(数据)传来,等待接水(接收数据)。)
3、父子进程实现ps、grep命令
>touch pipe_ps.c
>vi pipe_ps.c
1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 int fd[2]; 7 pipe(fd); 8 pid_t pid = fork(); 9 10 if(pid == 0){ 11 //son 12 //son --> ps 13 //1.先重定向 14 dup2(fd[1],STDOUT_FILENO);//标准输出重定向到管道写端 15 //2.execlp 16 execlp("ps","ps","aux",NULL);//ps aux标准输出 17 } 18 else if(pid > 0){ 19 //parent 20 //1.先重定向,标准输入重定向到管道读端 21 dup2(fd[0],STDIN_FILENO); 22 //2.execlp 23 execlp("grep","grep","bash",NULL);//grep bash等待标准输入 24 } 25 return 0; 26 }
>make
>./pipe_ps
(ps没有退,产生了一个僵尸进程!)
4、ps、grep命令实现问题解决
pipe_ps.c代码的问题:1)产生了僵尸进程ps;2)父进程认为还有写端存在,就有可能还有人给发数据,继续等待。
>vi pipe_ps.c
1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 int fd[2]; 7 pipe(fd); 8 pid_t pid = fork(); 9 10 if(pid == 0){ 11 //son 12 //son --> ps 13 //关闭读端 14 close(fd[0]); 15 //1.先重定向 16 dup2(fd[1],STDOUT_FILENO);//标准输出重定向到管道写端 17 //2.execlp 18 execlp("ps","ps","aux",NULL); 19 } 20 else if(pid > 0){ 21 //parent 22 //关闭写端 23 close(fd[1]); 24 //1.先重定向,标准输入重定向到管道读端 25 dup2(fd[0],STDIN_FILENO); 26 //2.execlp 27 execlp("grep","grep","bash",NULL);//grep bash等待标准输入 28 } 29 return 0; 30 }
>make
>./pipe_ps
5、管道的读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
总结:
① 读管道:1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道:1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。
读管道:
写端全部关闭——read读到0,相当于读到文件末尾
写端没有全部关闭
有数据——read 读到数据
没有数据——read 阻塞 ,fcntl 函数可以更改非阻塞
写管道:
读端全部关闭——?产生一个信号SIGPIPE,程序异常终止。
读端未全部关闭
管道已满——write阻塞。如果要显示现象,读端一直不读,写端狂写。
管道未满——write正常写入
测试:(写端全部关闭——read读到0,相当于读到文件末尾)
>vi pipe.c
1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 int fd[2];//声明数组 7 pipe(fd);//创建管道 8 pid_t pid = fork();//创建子进程 9 10 if(pid == 0){ 11 //son 12 sleep(3); 13 close(fd[0]);//关闭读端 14 write(fd[1],"hello",5); 15 close(fd[1]); 16 while(1){ 17 sleep(1); 18 } 19 } 20 else if(pid > 0){ 21 //parent 22 close(fd[1]);//关闭写端 23 char buf[12]={0}; 24 while(1){ 25 26 int ret = read(fd[0],buf,sizeof(buf));//read会等待write写 27 if(ret == 0){ 28 printf("read over!\n"); 29 break; 30 } 31 if(ret > 0){ 32 write(STDOUT_FILENO,buf,ret);//相当于printf,打印 33 } 34 } 35 36 } 37 return 0; 38 }
>make
>./pipe
(这时候打开另一个终端,ps aux查看子进程仍然活着,使用kill -9 pid杀死)
测试:(读端全部关闭——?产生一个信号SIGPIPE,程序异常终止。)
>vi pipe.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/wait.h> 5 6 int main() 7 { 8 int fd[2];//声明数组 9 pipe(fd);//创建管道 10 pid_t pid = fork();//创建子进程 11 12 if(pid == 0){ 13 //son 14 sleep(3); 15 close(fd[0]);//关闭读端 16 write(fd[1],"hello",5); 17 close(fd[1]); 18 while(1){ 19 sleep(1); 20 } 21 } 22 else if(pid > 0){ 23 //parent 24 close(fd[1]);//关闭写端 25 close(fd[0]); 26 27 int status; 28 wait(&status); 29 if(WIFSIGNALED(status)){ 30 printf("killed by %d\n", WTERMSIG(status)); 31 } 32 33 //父进程只是关闭读写两端,但是不退出 34 while(1){ 35 sleep(1); 36 } 37 38 char buf[12]={0}; 39 while(1){ 40 41 int ret = read(fd[0],buf,sizeof(buf));//read会等待write写 42 if(ret == 0){ 43 printf("read over!\n"); 44 break; 45 } 46 if(ret > 0){ 47 write(STDOUT_FILENO,buf,ret);//相当于printf,打印 48 } 49 } 50 51 } 52 return 0; 53 }
>make
>./pipe
输出:kill by 13
(这时候打开另一个终端,kill -l查看kill的13号信号的标识为:SIGPIPE)
6、管道大小和优劣
》计算管道大小
可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
pipe size (512 bytes, -p) 8
也可以使用fpathconf函数,借助参数选项来查看。使用该宏应引入头文件<unistd.h>
>man fpathconf
long fpathconf(int fd, int name);
成功:返回管道的大小失败:-1,设置errno
》优缺点
优点:简单
缺点:1)只能有血缘关系的进程通信;2)父子进程只能单方向通信,如果需要双向通信,需要创建多根管道。
三、进程通信——FIFO
1、fifo实现通信写端
FIFO:有名管道,实现无血缘关系进程通信
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
》创建一个管道的伪文件:
法一:
>mkfifo myfifo
>ls -lrt
(prw-rw-r-- 1 wang wang 0 7月 2 12:36 myfifo)
法二:可以用函数创建
>man 3 mkfifo
int mkfifo(const char *pathname, mode_t mode);
》内核会针对fifo文件开辟一个缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写
测试:
>touch fifo_w.c
>vi fifo_w.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<string.h> 7 8 int main(int argc, char *argv[]) 9 { 10 if(argc != 2){ 11 printf("./a.out fifoname\n"); 12 return -1; 13 } 14 //当前目录有一个myfifo文件 15 //打开fifo文件 16 int fd = open(argv[1],O_WRONLY); 17 //写 18 char buf[256]; 19 int num = 1; 20 while(1){//循环写 21 memset(buf,0x00,sizeof(buf)); 22 sprintf(buf,"xiaoming%04d",num++); 23 write(fd,buf,strlen(buf)); 24 sleep(1); 25 } 26 27 //关闭描述符 28 close(fd); 29 return 0; 30 }
>touch myfifo
>touch fifo_r.c
>vi fifo_r.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<string.h> 7 8 int main(int argc, char *argv[]) 9 { 10 if(argc != 2){ 11 printf("./a.out fifoname\n"); 12 return -1; 13 } 14 //当前目录有一个myfifo文件 15 //打开fifo文件 16 int fd = open(argv[1],O_RDONLY); 17 //写 18 char buf[256]; 19 int ret; 20 while(1){//循环读
memset(buf, 0x00,sizeof(buf)); 21 ret = read(fd,buf,sizeof(buf)); 22 if(ret > 0){ 23 printf("read:%s\n",buf); 24 } 25 } 26 27 //关闭描述符 28 close(fd); 29 return 0; 30 }
>make
>./fifo_w
(这时候打开另一个终端,./fifo_r读myfifo种的数据。也可以打开多个终端写,多个终端读,抢占读/写数据)
2、fifo使用注意事项
>vi fifo_w.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<string.h> 7 8 int main(int argc, char *argv[]) 9 { 10 if(argc != 2){ 11 printf("./a.out fifoname\n"); 12 return -1; 13 } 14 //当前目录有一个myfifo文件 15 //打开fifo文件 16 printf("begin open....\n"); 17 int fd = open(argv[1],O_WRONLY); 18 printf("end open....\n"); 19 //写 20 char buf[256]; 21 int num = 1; 22 while(1){//循环写 23 memset(buf,0x00,sizeof(buf)); 24 sprintf(buf,"xiaoming%04d",num++); 25 write(fd,buf,strlen(buf)); 26 sleep(1); 27 } 28 29 //关闭描述符 30 close(fd); 31 return 0; 32 }
>vi fifo_r.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<string.h> 7 8 int main(int argc, char *argv[]) 9 { 10 if(argc != 2){ 11 printf("./a.out fifoname\n"); 12 return -1; 13 } 14 //当前目录有一个myfifo文件 15 //打开fifo文件 16 printf("begin open read....\n"); 17 int fd = open(argv[1],O_RDONLY); 18 printf("begin open end....\n"); 19 //写 20 char buf[256]; 21 int ret; 22 while(1){//循环读 23 memset(buf, 0x00,sizeof(buf)); 24 ret = read(fd,buf,sizeof(buf)); 25 if(ret > 0){ 26 printf("read:%s\n",buf); 27 } 28 } 29 30 //关闭描述符 31 close(fd); 32 return 0; 33 }
>make
>./fifo_w
(这时候打开另一个终端,./fifo_r读myfifo种的数据。也可以打开多个终端写,多个终端读,抢占读/写数据)
FIFOs
Opening the read or write end of a FIFO blocks until the other end is also opened (by another process or thread.) See fifo (7) for further details.
open注意事项:打开fifo文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另外一端打开。
>man 7 fifo
查看fifo信息
四、进程通信——mmap
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
1、mmap映射开始
>man mmap
》创建映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr 传 NULL
length 映射区的长度
prot
PROT_READ 可读
PROT_WRITE 可写
flags
MAP_SHARED 共享的,对内存的修改会影响到源文件
MAP_PRIVATE 私有的
fd 文件描述符,open打开一个文件
offset 偏移量
返回值:成功返回可用的内存首地址;失败返回MAP_FAILED
》释放映射区
同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
借鉴malloc和free函数原型,尝试装自定义函数smalloc,sfree来完成映射区的建立和释放。思考函数接口该如何设计?
int munmap(void *addr, size_t length);
addr 传mmap的返回值
length mmap创建的长度
返回值:成功返回0,失败返回-1
练习:
>touch mem.txt
>vi mem.txt
xxxxxxxxxxxxx
>touch mmap.c
>vi mmap.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<string.h> 8 9 int main(int argc, char *argv[]) 10 { 11 int fd = open("mem.txt",O_RDWR); 12 13 //创建映射区 14 char *mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//如果是MAP_PRIVATE,不会更改源文件 15 16 if(mem == MAP_FAILED){ 17 perror("mmap error"); 18 return -1; 19 } 20 21 //拷贝数据 22 strcpy(mem,"hello"); 23 24 //释放mmap 25 munmap(mem, 8); 26 27 close(fd); 28 return 0; 29 }
>make
>./mmap
>cat mem.txt
2、mmap注意事项
解答:
(1)不能!!!(测试:mem++,然后释放,make后运行./mmap)
(2)文件的大小对映射区操作有影响,尽量避免。(测试:写入helloworld,make后运行./mmap,cat mem.txt文件为xxxxxxxxxxxxx时结果为:helloworldxxx;mem.txt文件为xxxxxxxx时,make后运行./mmap,cat mem.txt结果为helloworl)
另外如果不越界,而是大于文件大小,结果如何?
文件的大小对映射区操作有影响,尽量避免。
测试:在(5)中修改ftruncate(fd,8);mmap中修改为20。(测试:写入helloworld,make后运行./mmap,cat mem.txt文件为xxxxxxxx时,make后运行./mmap,cat mem.txt结果为helloworl)
(3)不可以,offset必须是4k的整数倍。(测试:ps aux > mem.txt;然后ls -l mem.txt看下文件大小,将offset改为1000,make后运行./mmap报错:mmap error:Invalid argument)
注意:0也是4096的整数倍!!!
(可以用ls -l mmap.c,然后stat mmap.c查看文件大小及块数)
(4)没有影响。(测试:将close(fd);放到strcpy(mem,"hello");之前,make后运行./mmap)
(5)不可以用大小为0的文件。(测试:将打开文件更改为:int fd = open("mem.txt",O_RDWR|O_CREAT|O_TRUNC,0664);(创建并且截断文件,保证打开时大小为0)make后./mmap报错:总线错误(核心已转储);在其后增加代码:ftruncate(fd,8);扩展文件大小,cat mem.txt结果为hellowor)
(6)不可以,Permission denied没有权限,原因映射到内存,隐含一次读操作。(测试:int fd = open("mem.txt",O_RDWR);将打开文件时O_RDWR更改为:O_WRONLY;make后./mmap报错:mmap err: Permisson denied)
(7)不可以,Permission denied没有权限,原因后期修改数据MAP_SHARED,可以修改源文件。(测试:int fd = open("mem.txt",O_RDWR);将打开文件时O_RDWR更改为:O_RDONLY;make后./mmap报错:mmap err: Permisson denied)
所以:open文件时候权限应该大于等于mmap时的权限!!!
(8)很多情况。
(9)会死的很难看 o(∩_∩)o 哈哈
总结:使用mmap时务必注意以下事项:
1. 创建映射区的过程中,隐含着一次对映射文件的读操作。
2. 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!! mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
6. 文件偏移量必须为4K的整数倍
7. mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
3、mmap实现父子进程通信
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;
MAP_SHARED: (共享映射) 父子进程共享映射区;
练习:
>touch mem.txt
>vi mem.txt
xxxxx
>touch mmap_child.c
>vi mmap_child.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<sys/wait.h> 8 9 int main(int argc, char *argv[]) 10 { 11 //先打开文件(有源文件) 12 int fd = open("mem.txt",O_RDWR); 13 14 //创建映射区 15 int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); 16 17 if(mem == MAP_FAILED){ 18 perror("mmap error"); 19 return -1; 20 } 21 22 //fork子进程 23 pid_t pid = fork(); 24 25 //父进程和子进程交替修改数据 26 if(pid == 0){ 27 //son 28 *mem = 100; 29 printf("child,*mem = %d\n",*mem); 30 sleep(3); 31 printf("child,*mem = %d\n",*mem); 32 } 33 else if(pid > 0){ 34 //parent 35 sleep(1); 36 printf("parent,*mem = %d\n",*mem); 37 *mem = 1001; 38 printf("parent,*mem = %d\n",*mem); 39 wait(NULL);//回收子进程 40 } 41 42 //释放mmap 43 if(munmap(mem, 4) < 0){ 44 perror("munmap err"); 45 } 46 47 close(fd); 48 return 0; 49 }
>make
>./mmap_child
》结论:父子进程共享:1. 打开的文件 2. mmap建立的映射区(但必须要使用MAP_SHARED)
4、匿名映射
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。
缺点:匿名映射无法实现无血缘关系的进程的通信!!!
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
"4"随意举例,该位置表大小,可依实际需要填写。
测试:
>touch mmap_anon.c
>vi mmap_anon.c
(重定向头文件:head -7 mmap_child.c > mmap.anon.c)
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<sys/wait.h> 8 9 int main(int argc, char *argv[]) 10 { 11 //创建映射区 12 int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0); 13 14 if(mem == MAP_FAILED){ 15 perror("mmap error"); 16 return -1; 17 } 18 19 //fork子进程 20 pid_t pid = fork(); 21 22 //父进程和子进程交替修改数据 23 if(pid == 0){ 24 //son 25 *mem = 101; 26 printf("child,*mem = %d\n",*mem); 27 sleep(3); 28 printf("child,*mem = %d\n",*mem); 29 } 30 else if(pid > 0){ 31 //parent 32 sleep(1); 33 printf("parent,*mem = %d\n",*mem); 34 *mem = 1001; 35 printf("parent,*mem = %d\n",*mem); 36 wait(NULL);//回收子进程 37 } 38 39 //释放mmap 40 if(munmap(mem, 4) < 0){ 41 perror("munmap err"); 42 } 43 44 close(fd); 45 return 0; 46 }
>make
>./mmap_anon
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。
》/dev/zero 聚宝盆,可以随意映射
》/dev/null 无底洞,一般错误信息重定向到这个文件中,不会占用磁盘空间。
在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
1)fd = open("/dev/zero", O_RDWR);
2)p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
5、mmap实现无血缘关系进程通信
实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。
>touch mmap_w.c
>vi mmap_w.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<sys/wait.h> 8 9 typedef struct _Student{ 10 int sid; 11 char sname[20]; 12 }Student; 13 14 15 int main(int argc, char *argv[]) 16 { 17 if(argc != 2){ 18 printf("./a.out filename\n"); 19 return -1; 20 } 21 22 //1.open file 23 int fd = open(argv[1],O_RDWR|O_CREATE|O_TRUNC,0666); 24 int length = sizeof(Student); 25 26 ftruncate(fd,length);//指定大小 27 //2.mmap 28 Student *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); 29 30 if(stu == MAP_FAILED){ 31 perror("mmap error"); 32 return -1; 33 } 34 35 //3.修改内存数据 36 int num = 1; 37 while(1){ 38 stu->sid = num; 39 sprintf(stu->sname,"xiaoming-%03d",num++); 40 sleep(1);//相当于每隔1s修改一次映射区的内容 41 } 42 43 44 //4.释放映射区,关闭文件描述符 45 if(munmap(stu, length) < 0){ 46 perror("munmap err"); 47 } 48 close(fd); 49 50 return 0; 51 }
>touch mmap_r.c
>vi mmap_r.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<sys/wait.h> 8 9 typedef struct _Student{ 10 int sid; 11 char sname[20]; 12 }Student; 13 14 15 int main(int argc, char *argv[]) 16 { 17 if(argc != 2){ 18 printf("./a.out filename\n"); 19 return -1; 20 } 21 22 //1.open file 23 int fd = open(argv[1],O_RDWR); 24 int length = sizeof(Student); 25 26 //2.mmap 27 Student *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); 28 29 if(stu == MAP_FAILED){ 30 perror("mmap error"); 31 return -1; 32 } 33 34 //3.read data 35 while(1){ 36 printf("sid=%d,sname=%s\n",stu->sid,stu->sname); 37 sleep(1);//相当于每隔1s修改一次映射区的内容 38 } 39 40 41 //4.close and munmap 42 if(munmap(stu, length) < 0){ 43 perror("munmap err"); 44 } 45 close(fd); 46 47 return 0; 48 }
>make
>./mmap_w xxx
(这时候打开另一个终端,./mmap_r xxx查看;多开终端,读到的结果一样!)
原理图分析:(映射两块不同的内存!)
6、mmap(MAP_SHSRED)再次说明
>vi mmap_child.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<sys/wait.h> 8 9 int main(int argc, char *argv[]) 10 { 11 //先打开文件(有源文件) 12 int fd = open("mem.txt",O_RDWR); 13 14 //创建映射区 15 int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0); 16 17 if(mem == MAP_FAILED){ 18 perror("mmap error"); 19 return -1; 20 } 21 22 //fork子进程 23 pid_t pid = fork(); 24 25 //父进程和子进程交替修改数据 26 if(pid == 0){ 27 //son 28 *mem = 100; 29 printf("child,*mem = %d\n",*mem); 30 sleep(3); 31 printf("child,*mem = %d\n",*mem); 32 } 33 else if(pid > 0){ 34 //parent 35 sleep(1); 36 printf("parent,*mem = %d\n",*mem); 37 *mem = 1001; 38 printf("parent,*mem = %d\n",*mem); 39 wait(NULL); 40 } 41 42 //释放mmap 43 if(munmap(mem, 4) < 0){ 44 perror("munmap err"); 45 } 46 47 close(fd); 48 return 0; 49 }
>make
>./mmap_chilid
(查看父子进程的数据,得知MAP_PRIVATE,无法共享)
如果进程要通信,flags必须设为MAP_SHARED,否则无法通信!
作业
》多进程拷贝
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 #include<sys/wait.h> 8 #include<stdlib.h> 9 #include<string.h> 10 11 int main(int argc, char *argv[]) 12 { 13 int n = 5; 14 //输入参数至少是3,第4个参数可以是进程个数 15 if(argc < 3){ 16 printf("./a.out src dst [n]\n"); 17 return 0; 18 } 19 if(argc == 4){ 20 n = atoi(argv[3]); 21 } 22 //打开源文件 23 int srcfd = open(argv[1],O_RDONLY); 24 if(srcfd < 0){ 25 perror("open err"); 26 exit(1); 27 } 28 //打开目标文件 29 int dstfd = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664); 30 if(dstfd < 0){ 31 perror("open dst err"); 32 exit(1); 33 } 34 //目标拓展,从原文件获得文件大小,stat 35 struct stat sb; 36 stat(argv[1],&sb);//为了计算大小 37 int len = sb.st_size; 38 truncate(argv[2],len); 39 //将源文件映射到缓冲区 40 char *psrc = mmap(NULL, len, PROT_READ,MAP_SHARED,srcfd,0); 41 if(pdst == MAP_FAILED){ 42 perror("mmap dst err"); 43 exit(1); 44 } 45 //创建多个子进程 46 int i = 0; 47 for(i = 0; i < n; i++){ 48 if(fork() == 0){ 49 break; 50 } 51 } 52 //计算子进程需要拷贝的起点和大小 53 int cpsize = len/n; 54 int mod = len%n; 55 //数据拷贝,memcpy 56 if(i < n){//子进程 57 if(i == n-1){//最后一个子进程 58 //(首地址,源地址,大小) 59 memcpy(pdst+i*cpsize,psrc+i*cpsize,cpsize+mod); 60 } 61 else{ 62 memcpy(pdst+i*cpsize,psrc+i*cpsize,cpsize); 63 } 64 } 65 else{ 66 for(i = 0; i < n; i++){ 67 wait(NULL); 68 } 69 } 70 //释放映射区 71 if(munmap(mem, len) < 0){ 72 perror("munmap dst err"); 73 exit(1); 74 } 75 //关闭文件 76 close(srcfd); 77 close(dstfd); 78 return 0; 79 }
五、进程通信——信号
1、信号的概念
信号的特点:简单,不能带大量信息,满足特定条件发生。
信号的机制:进程B发送给进程A,内核产生信号,内核处理。
信号的产生:
按键产生:Ctrl+c,Ctrl+z,Ctrl+\
调用函数:kill,raise,abort
定时器:alarm,setitimer
命令产生:kill
硬件异常:段错误,浮点型错误,总线错误,SIGPIPE
信号的状态:
产生
递达:信号到达并且处理完
未决:信号被阻塞了
信号的默认处理方式:
忽略
执行默认动作
捕获
信号的4要素:
编号
事件
名称
默认处理动作(可通过man 7 signal查看):忽略,终止,终止+core,暂停,继续
kill -l 可以查看信号种类,一共64个(只学习前31个)。
在学习Linux系统编程总结了笔记,并分享出来。有问题请及时联系博主:Alliswell_WP,转载请注明出处。
posted on 2020-07-01 11:57 Alliswell_WP 阅读(351) 评论(0) 编辑 收藏 举报