Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(InterProcess Communication,IPC)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
(1)管道(使用最简单)
(2)信号(开销最小,后续再进行详细介绍)
(3)共享映射区(无血缘关系)
(4)本地套接字(最稳定,后续再进行详细介绍)
一、管道
1 pipe匿名管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
(1)其本质是一个伪文件(实为内核缓冲区)
(2)由两个文件描述符引用,一个表示读端,一个表示写端。
(3)规定数据从管道的写端流入管道,从读端流出。
管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
(1)数据自己读不能自己写。
(2)数据一旦被读走,便不在管道中存在,不可反复读取。
(3)由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
(4)只能在有公共祖先的进程间使用管道。
管道的通信流程如下所示:
1.父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
2.父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
3.父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
#include <unistd.h>
int pipe(int filedes[2]);
描述:
管道作用于有血缘关系的进程之间,通过fork来传递调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
代码示例:
#include <stdlib.h>//perror
#include <unistd.h>//pipe
#include <stdio.h>
#include <sys/wait.h>//wait
#define MAXLINE 80
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0) {
perror("pipe");
exit(1);
}
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid > 0) { /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
wait(NULL);
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
return 0;
}
2 fifo有名管道
fifo常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过fifo,不相关的进程也能交换数据。
创建方式:
(1)命令方式:mkfifo xwp
(2)函数方式:mkfifo函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
mode与open函数的mode功能一样,创建成功返回0,失败返回-1。
代码示例:
#include <stdlib.h>//perror
#include <stdio.h>
#include <sys/stat.h>//mkfifo
#define MAXLINE 80
int main(void)
{
int ret = mkfifo("myfifo",0664);
if(ret == -1)
{
perror("mkfifo");
}
return 0;
}
执行后可以看到多了一个管道文件:
创建完管道文件后,与读写文件类似的,在一个进程中写管道,另一个进程中读管道,则可以实现进程间的通信,这里不再赘述。
需要注意的几个点如下:
- 当只写打开FIFO管道时,如果没有FIFO没有读端打开,则open写打开会阻塞。
- FIFO内核实现时可以支持双向通信。(pipe单向通信,因为父子进程共享同一个file结构体)
- FIFO可以一个读端,多个写端;也可以一个写端,多个读端。(请测试)
二、内存共享映射:
1 mmap/munmap
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); //创建贡献内存映射
int munmap(void *addr, size_t length);
参数:
addr :指定映射区的首地址。通常传NULL,内核会自己在进程地址空间中选择合适的地址建立映射。如果addr不是NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择addr之上的某个合适的地址开始映射。建立映射后,真正的映射首地址通过返回值可以得到。
length:需要映射的那一部分文件的长度。(不大于文件的实际大小)
prot:有四种取值,表示读写权限
- PROT_EXEC表示映射的这一段可执行,例如映射共享库
- PROT_READ表示映射的这一段可读
- PROT_WRITE表示映射的这一段可写
- PROT_NONE表示映射的这一段不可访问
flags:参数有很多种取值,这里只讲两种
- MAP_SHARED多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化。
- MAP_PRIVATE多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去。
fd:用于创建共享内存映射区的那个文件的文件描述符。
offset:偏移位置,默认为0表示映射文件的所有。必须是4k的整数倍。
返回值:
如果mmap成功则返回映射首地址,如果出错则返回常数MAP_FAILED。当进程终止时,该进程的映射内存会自动解除,也可以调用munmap解除映射。munmap成功返回0,出错返回-1。
#include <stdlib.h>//perror
#include <sys/mman.h>//mmap
#include <string.h>//strcpy
#include <stdio.h>
#include <unistd.h>//lseek/ftruncate
int main(void)
{
char *p;
int fd = open("hello", O_RDWR|O_CREAT|O_TRUNC,0664);
if (fd < 0) {
perror("open hello");
exit(1);
}
ftruncate(fd,20);//创建大小为20字节的文件,需要写权限
int len = lseek(fd,0,SEEK_END);//获取文件长度
p = mmap(NULL, len, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap");
exit(1);
}
close(fd);
strcpy(p,"hello mmap");//写操作,将同步修改磁盘文件内容
return 0;
}
2 mmap注意事项
- 用于创建映射区的文件大小为0,实际制定非0大小创建映射区,出总线错误。
- 用于创建映射区的文件大小为0,实际制定0大小创建映射区,出无效参数错误。
- 用于创建映射区的文件读写属性为只读,映射区属性为读写,出权限错误。
- 文件描述符fd在mmap创建映射区后即可关闭。后续访问文件用地址访问。
- 创建映射区,需要读权限。“共享”MAP_PRIVATE时,mmap的读写权限(第三个参数prot指定)应该<=文件open时指定的权限。只写不行。
- offset必须为4k的整数倍,否则报无效参数错误。
- 对申请的内存,不能越界访问。否则报段错误。
- mmap用于释放的地址,必须是mmap返回的地址。若需要通过修改指针(p++)访问内存,则可以用临时变量存储初始位置再执行修改指针操作(tmp=p;p++),方便munmap释放映射mumap(tmp,len)。
- 映射区访问权限为“私有”MAP_PRIVATE,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
- 映射区访问权限为“私有”MAP_PRIVATE,open文件时,只需要有读权限用于穿件映射区即可。
mmap函数的保险调用方式:
- open(O_RDWR)
- mmap(NULL,实际文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
3 使用mmap进程间通信
3.1 父子间通信
(1)有名映射
#include <stdlib.h>//perror
#include <sys/mman.h>//mmap
#include <sys/wait.h>//wait
#include <string.h>//strcpy
#include <stdio.h>
#include <unistd.h>//lseek/ftruncate
#include <fcntl.h>//flags
int main(void)
{
int var = 100;
int *p;
int fd = open("hello", O_RDWR|O_CREAT|O_TRUNC,0664);
if (fd < 0) {
perror("open hello");
exit(1);
}
ftruncate(fd,20);//创建大小为20字节的文件,需要写权限
int len = lseek(fd,0,SEEK_END);//获取文件长度
p = (int *)mmap(NULL, len, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap");
exit(1);
}
close(fd);
pid_t pid = fork();
if(pid == 0)//子进程
{
*p = 2000; //向共享映射区写入2000
var = 1000;
printf("child, *p=%d, var=%d\n",*p,var);
}
else
{
sleep(1);
printf("parent,*p=%d,var=%d\n",*p,var);//读共享映射区
int ret = munmap(p,20);//释放映射区
if(ret == -1)
{
perror("munmap error");
exit(1);
}
}
return 0;
}
(2)匿名映射
#include <stdlib.h>//perror
#include <sys/mman.h>//mmap
#include <sys/wait.h>//wait
#include <string.h>//strcpy
#include <stdio.h>
#include <unistd.h>//lseek/ftruncate
#include <fcntl.h>//flags
int main(void)
{
int var = 100;
int *p;
p = (int *)mmap(NULL, 40, PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) {
perror("mmap");
exit(1);
}
pid_t pid = fork();
if(pid == 0)//子进程
{
*p = 2000; //向共享映射区写入2000
var = 1000;
printf("child, *p=%d, var=%d\n",*p,var);
}
else
{
sleep(1);
printf("parent,*p=%d,var=%d\n",*p,var);//读共享映射区
int ret = munmap(p,20);//释放映射区
if(ret == -1)
{
perror("munmap error");
exit(1);
}
}
return 0;
}
3.2 无血缘关系进程通信
写端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
struct student {
int id;
char name[20];
int age;
};
void sys_err(char* s) {
perror(s);
exit(1);
}
int main() {
int fd;
struct student stu = {10, "xiaoming", 18};
struct student* mm;
if ((fd = open("tmpfile", O_RDWR | O_CREAT | O_TRUNC, 0644)) < 0)
sys_err("open tmpfile");
ftruncate(fd, sizeof(stu));
mm = mmap(NULL, sizeof(stu), PROT_WRITE, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap failed");
close(fd);
while (stu.id++ < 100) {
printf("id = %d, name = %s, age= %d \n", mm->id, mm->name, mm->sex);
memcpy(mm, &stu, sizeof(stu));
sleep(1);
}
unlink("tmpfile");
munmap(mm, sizeof(stu));
return 0;
}
读端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
struct student {
int id;
char name[20];
int sex;
};
void sys_err(char* s) {
perror(s);
exit(1);
}
int main() {
int fd;
struct student* mm;
if ((fd = open("tmpfile", O_RDONLY)) < 0)
sys_err("open tmpfile");
ftruncate(fd, sizeof(struct student));
mm = mmap(NULL, sizeof(struct student), PROT_READ, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap failed");
close(fd);
while (1) {
printf("id = %d, name = %s, age= %d \n", mm->id, mm->name, mm->sex);
sleep(1);
}
munmap(mm, sizeof(struct student));
return 0;
}
连个进程打开同一个文件,创建映射区。
是定flags为MAP_SHARED.
一个进程写入,另一个读出。
【注意】
mmap:数据可以重复读取。
fifo:数据只能一次读取
另外,可以使用/dev/zero文件进行通信,而不需要创建。任何程序可以从其中取出无线大小的空洞内容。而/dev/null相反,可以向其中写入无限大小的内容。