进程间通信 & 管道
IPC工具分类
通信: 关注进程间数据交换
同步: 进程和线程间的同步
信号:
通信工具
数据传输工具: 为了进行通信,一个进程将数据写入到IPC工具中,另一个进程从中读取数据。这些工具要求在用户内存和内核内存之间进行两次数据传输
共享内存: 进程通过将数据放到由进程间共享的一块内存中以完成信息的交换(内核通过将每个进程的页表条目指向同一个RAM分页来实现这一功能)
数据传输
字节流
消息
伪终端
同步工具
信号量: 一个信号量是一个由内核维护的整数。其值永远不会小于0.如果一个进程试图将信号量的值减小到小于0,那么内核会阻塞直到信号量的值增长到允许执行该操作的程序
文件锁:用来协调操作同一文件的多个进程的动作的一种同步方法。文件锁分为两类:读(共享)锁和写(互斥)锁。linux通过flock()和fcntl()提供文件加锁工具
互斥体和条件变量
要访问一个IPC对象,进程必须要通过某种方式来标识出该对象,一旦家那个对象“打开”之后,进程必须要使用某种句柄来引用该打开着的对象
IPC工具的可访问性和持久性
持久性是指一个IPC工具的生命周期
进程持久性:只要存在一个进程持有进程持久的IPC对象,那么该对象的生命周期就不会终止。管道、FIFO、socket
内核持久性:只有当显示地删除内核持久的IPC对象或系统关闭时,该对象才会销毁。systemV IPC和POSIX IPC
文件系统持久性:会在系统重启时保存其中的信息,这种对象一直存在直至被显示删除。基于内存映射文件的共享内存
管道和FIFO
管道的重要特征
一个管道是一个字节流: 意味着在使用管道时是不存在消息或消息边界的概念的。从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。通过管道传递的数据时顺序的
从管道中读取数据: 试图从一个当前为空的管道中读取数据将会被阻塞直到至少有一个字节被写入到管道中。如果管道的写入端被关闭了,那么从管道中读取数据的进程在读完管道中剩余的数据后将会看到文件结束(read()返回0)
管道是单向的:
可以确保写入不超过PIPE_BUF字节的操作是原子的
管道的容量是有限的:管道其实是一个在内核内存中维护的缓冲区,这个缓冲区的存储能力是有限的。一旦管道被填满,写操作会被阻塞直到读者从管道中移除可一些数据
- 使用较大缓冲器的原因是效率:每当写者充满管道时,内核必须执行一个上下文切换以允许读者被调度来消耗管道中的一些数据。使用较大的缓冲器意味着需要执行的上下文切换次数少
创建和使用管道
#inlcude <unistd.h>
int pip3(int filedes[2]);
成功,pipe()会在filedes中返回两个打开的文件描述符,filedes[0]:读, filedes[1]:写
可以使用read()和write()在管道上执行IO
为了让两个进程通过管道进程连接,在调用完pipe()之后可以调用fork(),在fork()期间,子进程会继承父进程的我呢间描述符的副本。在调用fork()之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个则应该关闭读取端的描述符
使用管道将数据从父进程传输到子进程:
int filedes[2];
if(pipe(filedes)==-1)
errExit("pipe");
switch(fork()){
case -1:
errExit("fork");
case 0: //child
if(close(filedes[1])==-1) //close write
errExit("close");
break;
default:
if(close(filedes[0])==-1)
errExit("close");
break;
}
pipe2(),支持额外的参数,O_NONBLOCK ,内核将底层的打开文件描述符标记为非阻塞的
允许相关进程的通信:管道可用于任意两个或多个进程的通信,只要在创建进程的系列fork()调用之前通过一个共同的祖先进程创建管道即可
关闭未使用的文件描述符
- 从管道读取数据的进程会关闭其持有的管道的写入描述符,这样当其他进程完成输出并关闭写入描述符之后,读者就能看到文件结束
- 写者进程关闭其持有的读取描述符。当一个进程试图向一个管道中写入数据但没有任何进程拥有该管道的打开着的读取描述符时,内核会向写者进程发送SIGPIPE信号
- 只有当所有进程中所有引用一个管道的文件描述符被关闭之后才会销毁该管道以及释放该管道占用的资源以供其他进程复用
父子进程间使用管道通信
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 10
int main(int argc, char*argv[]){
int pfd[2];
char buf[BUF_SIZE];
ssize_t numRead;
if(argc!=2){
printf("arg error.\n");
return 0;
}
if(pipe(pfd)==-1){
printf("pipe error. \n");
return 0;
}
switch(fork()){
case -1:
printf("fork error \n");
return 0;
case 0:
if(close(pfd[1])==-1){
printf("close err. \n");
return 0;
}
for(;;){
numRead = read(pfd[0], buf, BUF_SIZE);
if(numRead==-1){
printf("read err.\n");
return 0;
}
if(numRead==0)
break;
if(write(STDOUT_FILENO, buf, numRead) != numRead){ //写到标准输出
printf("child failed write.\n");
return 0;
}
}
write(STDOUT_FILENO, "\n", 1);
if(close(pfd[0])==-1){
printf("close err.\n");
return 0;
}
return 0;
default:
if(close(pfd[0])==-1){
printf("close-parent err.\n");
return 0;
}
if(write(pfd[1], argv[1], strlen(argv[1]))!=strlen(argv[1])){
printf("parent write err.\n");
return 0;
}
if(close(pfd[1])==-1){
printf("close.\n");
return 0;
}
wait(NULL);
return 0;
}
}
管道--进程同步
父进程在创建子进程之前构建了一个管道。每个进程会继承管道的写入端文件描述符并在完成动作之后关闭这些描述符。当所有子进程都关闭了管道的写入端描述符后,父进程在管道上的read()就会结束并返回文件结束。这时父进程就能做其他工作了
#include <stdio.h>
#include <unistd.h>
#include <time.h>
int main(int argc, char*argv[]){
int pfd[2];
int j, dummy;
if(argc<2){
printf("arg error. \n");
return 0;
}
setbuf(stdout, NULL);
printf("%d Parent started.\n", time(NULL));
if(pipe(pfd)==-1){
printf("pipe err.\n");
return 0;
}
for(j=1; j<argc; j++){
switch (fork()){
case -1:
printf("fork err. \n");
return 0;
case 0:
if(close(pfd[0])==-1){
printf("close err.\n");
return 0;
}
//sleep(aito(argv[j]), GN_NONNEG, "sleep-time");
sleep(atoi(argv[j]));
printf("%d Child %d (PID+%ld) closing pipe\n", time(NULL), j, (long)getpid());
if(close(pfd[1])==-1){
printf("close err.\n");
return 0;
}
return 0;
default:
break;
}
}
if(close(pfd[1])==-1){
printf("close err.\n");
return 0;
}
if(read(pfd[0], &dummy, 1)!=0){
printf("parent didnot get EOF");
return 0;
}
printf("%d parent ready to go.\n", time(NULL));
return 0;
}
使用管道连接过滤器
当管道被创建后,为关东的两端分配的文件描述符是可用描述符中数值最小的两个。由于在通常情况下,进程已经使用了描述符0,1,2,因此会为管道分配一些数值更大的描述符
使用管道连接两个过滤器,即从stdin读取和写入到stdout,使得一个宏程序的标准输出被定向到管道,而另一个程序的标准输入则从管道中获取
复制文件描述符
int pfd[2]
pipe(pfd)
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
//在复制完pfd[1]之后就拥有两个引用管道的写入端的文件描述符了,描述符1和pfd[1]
创建两个子进程,第一个子进程将其标准输出绑定到管道的写入端,然后执行ls,第二个子进程将其标准输入绑定到管道的写入段,然后执行wc
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char*argv){
int pfd[2];
if(pipe(pfd)==-1){
printf("pipe err.\n");
return 0;
}
switch(fork()){
case -1:
printf("fork err\n");
return 0;
case 0:
if(close(pfd[0])==-1){
printf("close err.\n");
return 0;
}
if(pfd[1]!=STDOUT_FILENO){
if(dup2(pfd[1], STDOUT_FILENO)==-1){
printf("dup err.\n");
return 0;
}
if(close(pfd[1])==-1){
printf("close \n");
return 0;
}
}
execlp("ls", "ls", (char*)NULL);
printf("execlp ls err.\n");
return 0;
default:
break;
}
switch(fork()){
case -1:
printf("fork er.\n");
return 0;
case 0:
if(close(pfd[1])==-1){
printf("close err\n");
return 0;
}
if(pfd[0]!=STDIN_FILENO){
if(dup2(pfd[0], STDIN_FILENO)==-1){
printf("err dup \n");
return 0;
}
if(close(pfd[0])==-1){
printf("err close \n");
return 0;
}
}
execlp("wc", "wc", "-l", (char*)NULL);
printf("execlp err\n");
return 0;
default:
break;
}
if(close(pfd[0])==-1){
return 0;
}
if(close(pfd[1])==-1)
return 0;
if(wait(NULL)==-1)
return 0;
if(wait(NULL)==-1)
return 0;
return 0;
}
通过管道与shell命令通信: popen()
popen()函数创建一个管道,然后创建一个子进程来执行shell, shell又创建了一个子进程来执行command字符串
FIFO
FIFO与管道类似,两者之间最大的差别在于FIFO在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样的。这样就能将FIFO用于非相关进程之间的通信(客户端和服务端)
一旦打开了FIFO,就能在上面使用与操作管道和其他文件的系统调用一样的IO系统调用
mkfifo在shell中创建一个fifo
mkfifo [-m mode] pathname
使用FIFO和tee(1)创建双重管道线
mkfifo myfifo
wc -l < myfifo &
ls -l | tee myfifo | sort -k5n