1 内存壁垒
进程间天然存在内存壁垒,无法通过交换虚拟地址直接进行数据交换:
每个进程的用户空间都是0~3G-1(32位系统),但它们所对应的物理内存却是各自独立的。系统为每个进程的用户空间维护一张专属于该进程的内存映射表,记录虚拟内存到物理内存的对应关系,因此在不同进程之间交换虚拟内存地址是毫无意义的。
所有进程的内核空间都是3G~4G-1,它们所对应的物理内存只有一份,系统为所有进程的内核空间维护一张内存映射表init_mm.pgd,记录虚拟内存到物理内存的对应关系,因此不同进程通过系统调用所访问的内核代码和数据是同一份。
用户空间的内存映射表会随着进程的切换而切换,内核空间的内存映射表不变:
Unix/Linux系统(32位)中的每个进程都拥有4G字节大小的专属于自己的虚拟内存空间,出去内核空间的1G,每个进程都有一张独立的内存映射表(内存分页表)记录着虚拟内存页和物理内存页之间的映射关系。
同一个虚拟内存地址,在不同的进程中,会被映射到完全不同的物理内存区域,因此在多个进程之间以交换虚拟内存地址的方式交换数据是不可能的。
鉴于进程之间天然存在的内存壁垒,要想实现多个进程间的数据交换,就必须提供一种专门的机制,这就是所谓的进程间通信(InterProcessCommunication,IPC)
2 进程间通信(IPC)的种类
2.1 命令行参数
在通过exec ()函数创建新进程时,可以为其指定命令行参数——借助命令行参数,可将创建者进程的某些数据传入新进程
execl ("./login", "login", "username", "password", NULL);
2.2 环境变量
类似地,也可在调用exec ()函数时为新进程提供环境变量:
sprintf (envp[0], "USERNAME=%s", username);
sprintf (envp[1], "PASSWORD=%s", password);
execl ("./login", "login", NULL, envp);
2.3 内存映射文件
通信双方分别将自己的一段虚拟内存映射到同一个文件中:mmap()
2.4 管道
管道是Unix系统中最古老的进程间通信方式,并且所有的Unix系统和包括Linux系统在内的各种类Unix系统也都提供这种进程间通信机制。管道有2种限制:
1 管道都是半双工的,数据只能沿着一个方向流动,类似对讲机,而非手机。
2 管道只能在具有公共祖先的进程之间使用。通常一个管道由一个进程创建,然后该进程通
过fork()函数创建子进程,父子进程之间通过管道交换数据。
大多数Unix/Linux系统出了提供传统意义上的无名管道以外,还提供有名管道,对后者而言,第2中限制已不复存在。
2.5 共享内存
共享内存允许两个或两个以上的进程共享同一块给定的内存区域。因为数据不需要在通信诸方之间来回复制,所以这是速度最块的一种进程间通信方式。
2.6 消息队列
消息队列是由系统内核负责维护并可在多个进程之间共享存取的消息链表。优点是:
传输可靠、流量受控、面向有结构的记录、支持按类型过滤。
2.7 信号量
与共享内存和消息队列不同,信号量并不是为了解决进程间的数据交换问题。
信号量关注的是有限的资源如何在无限的用户间合理分配,即资源竞争问题。
2.8 本地套接字
BSD版本的有名管道。编程模型和网络通信统一。
3 有名管道(FIFO)
有名管道是一种特殊的文件,它的路径名存在于文件系统中。
有名管道文件在磁盘上只有i节点,没有数据块,也不保存数据。数据由内核操作。
3.1 mkfifo 命令
通过shell命令mkfifo,基于有名管道实现进程间通信的逻辑模型:
通过mkfifo命令可以创建有名管道文件:
$ mkfifo myfifo
即使是毫无亲缘关系的进程,也可以通过有名管道文件通信:
$ echo 'Hello, FIFO!' > myfifo
$ cat myfifo
Hello, FIFO!
3.2 mkfifo()函数
通过mkfifo()函数,基于有名管道实现进程间通信的编程模型:
#include <sys/stat.h>
int mkfifo (char const* pathname, mode_t mode);
功能:创建有名管道(文件)
pathname:有名管道文件的路径
mode:权限模式
返回值:成0败-1
//wfifo.c 写入有名管道文件
#include<stdio.h>
#include<string.h>
#include<unistd.h>// write() close()
#include<fcntl.h>// open()
#include<sys/stat.h>// mkfifo()
int main(void){
//创建有名管道
printf("%d进程:创建有名管道\n",getpid());
if(mkfifo("./fifo",0664) == -1){
perror("mkfifo");
return -1;
}
//打开有名管道
printf("%d进程:打开有名管道\n",getpid());
int fd = open("./fifo",O_WRONLY);
if(fd == -1){
perror("open");
return -1;
}
//写入有名管道
printf("%d进程:发送数据\n",getpid());
for(;;){
//通过键盘获取数据 scanf fgets read fread fscanf
char buf[64] = {};
fgets(buf,sizeof(buf),stdin); //这里不用减1,fgets()会自动减!!
//当输入!时退出循环
if(strcmp(buf,"!\n") == 0){
break;
}
//写入管道文件
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
}
//关闭有名管道
printf("%d进程:关闭有名管道\n",getpid());
close(fd);
//删除有名管道
printf("%d进程:删除有名管道\n",getpid());
unlink("./fifo");
printf("%d进程:大功告成\n",getpid());
return 0;
}//编译后,开两终端,一个执行wfifo,一个执行rfifo
//rfifo.c 读取有名管道文件
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开有名管道
printf("%d进程:打开有名管道\n",getpid());
int fd = open("./fifo",O_RDONLY);
if(fd == -1){
perror("open");
return -1;
}
//读取有名管道
printf("%d进程:接收数据\n",getpid());
for(;;){
//读取有名管道
char buf[64] = {};
ssize_t size = read(fd,buf,sizeof(buf) - 1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
printf("%d进程:对方关闭管道文件\n",getpid());
break;
}
//显示
printf("%s",buf);
}
//关闭有名管道
printf("%d进程:关闭有名管道\n",getpid());
close(fd);
printf("%d进程:大功告成\n",getpid());
return 0;
}//编译后,开两终端,一个执行wfifo,一个执行rfifo
4 无名管道(PIPE)
通过pipe()函数,基于无名管道实现进程间通信的编程模型(5步):
1)父进程调用pipe()函数在系统内核中创建无名管道对象,
并通过该函数的输出参数pipefd,
获得分别用于读写该管道的两个 文件描述符pipefd[0]和pipefd[1]。
2) 父进程调用fork()函数,创建子进程。
子进程复制父进程的文件描述符表,因此子进程同样持有pipefd[0]和pipefd[1] 。
3) 负责写数据的进程关闭无名管道对象的读端描述符pipefd[0],
复测读数据的进程关闭无名管道对象的写端描述符pipefd[1]。
4)父子进程通过无名管道对象以半双工的方式传输数据。
如果需要在父子进程间双向通信,一般会创建两个管道,一个从父流向子,一个相反。
5)父子进程分别关闭自己所持有的写端或读端文件描述符。
在相关联的所有文件描述符都被关闭后,该无名管道对象即从内核中被销毁。
//pipe.c 无名管道演示
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(void){
//父进程创建无名管道
printf("%d进程:创建无名管道\n",getpid());
int fd[2];//用来输出管道读端写端描述符
if(pipe(fd) == -1){
perror("pipe");
return -1;
}
printf("fd[0] = %d\n",fd[0]);
printf("fd[1] = %d\n",fd[1]);
//父进程创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程代码,从管道中读取数据
if(pid == 0){
printf("%d进程:接受数据\n",getpid());
printf("%d进程:关闭写端\n",getpid());
close(fd[1]);
for(;;){
//通过读端描述符读取数据
char buf[64] = {};
ssize_t size = read(fd[0],buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
printf("%d进程:对方关闭写端描述符\n",getpid());
break;
}
//显示
printf("--->%s",buf);
}
printf("%d进程:关闭读端\n",getpid());
close(fd[0]);
printf("%d进程:大功告成\n",getpid());
return 0;//!!!!
}
//父进程代码,向管道中写入数据
printf("%d进程:发送数据\n",getpid());
printf("%d进程:关闭读端\n",getpid());
close(fd[0]);
for(;;){
//通过键盘获取数据
char buf[64] = {};
fgets(buf,sizeof(buf),stdin);
//!退出
if(strcmp(buf,"!\n") == 0){
break;
}
//通过管道写端写入
if(write(fd[1],buf,strlen(buf)) == -1){
perror("write");
return -1;
}
}
printf("%d进程:关闭写端\n",getpid());
close(fd[1]);
//父进程收尸
if(wait(NULL) == -1){
perror("wait");
return -1;
}
printf("%d进程:大功告成\n",getpid());
return 0;
}//编译执行
5 管道须知
1)从写端已被关闭的管道读取,只要管道中还有数据,依然可以被正常读取,一致到管道中没有数据了,这时read()函数会返回0(不是返回-1,也不是阻塞),指示读到文件尾。
2)向读端已被关闭的管道写入,会直接出发SIGPIPE(13)信号。该信号的默认操作是终止执行写入动作的进程。但如果执行写入动作的进程事先13信号的处理设置为忽略或捕获,则write()函数会返回-1,并置errno为EPIPE。
3)系统内核通常为每个管道维护一个4096字节的内存缓冲区(新系统更大)。如果写管道时发现缓冲区的空闲空间不足以容纳此次write()函数所要写入的字节,则write()函数阻塞,直到缓冲区的空闲空间变得足够大为止。
4)读取一个写段处于开放状态的空管道,直接导致read()函数阻塞。
6 管道符 | 的原理
1)Unix/Linux系统中的多数shell环境都支持,
通过管道符号 "|" 将前一个命令的输出作为后一个命令的输入:
$ ls -l /etc | more 实现按空格键翻页
$ ifconfig | grep inet 过滤得到ip地址
2)系统管理员通常用这种方法,把多个简单的命令连接成一条工具链,解决复杂问题:
$ 命令1 | 命令2 | 命令3
3)假设用户输入以下命令:a | b,管道符工作原理如下:
Shell进程调用fork()函数创建子进程A
子进程A调用pipe()函数创建无名管道,而后执行:
dup2 (pipefd[1], STDOUT_FILENO);
子进程A调用fork()函数创建孙进程B,孙进程B执行:
dup2 (pipefd[0], STDOUT_FILENO);
子进程A和孙进程B分别调用exec ()函数创建a、b进程。
a进程所有的输出都通过写段进入管道,而b进程所有的输入则来自管道的读端。