转载请注明出处: http://blog.csdn.net/suool/article/details/38444149, 谢谢!
在Linux系统中,进程是一个独立的资源管理单元,但是独立而不孤立,他们需要之间的通信,因此便需要一个进程间数据传递、异步、同步的机制,这个机制显然需要由OS来完成管理和维护。如下:
1、同一主机进程间数据交互机制:无名管道(PIPE),有名管道(FIFO),消息队列(Message Queue)和共享内存(Share Memory)。无名管道多用于亲缘关系的进程间通信,但是管道位单向,多进程使用一个同一个管道导致交叉读写。消息队列可以实现同主机上任意多的进程间通信,但是消息队列可存放的数据量有限,用与少量的数据传递。共享内存可以实现同主机任意进程间的大量数据传递,但因为共享数据空间访问时存在竞争问题。
2、同主机进程间同步机制:信号量(semaphore)
3、同主机进程间异步机制:信号(signal)
4、网络主机间数据交互机制:套接口(Socket)
本节主要记录UNIX进程间通信机制:PIPE,FIFO。
What's 管道?
管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
管道的实现机制:
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
使用管道的示例:rpm -qa | grep telnet
使用管道”|“可以将两个命令连接起来,从而改变标准的输入输出方式。在下面的shell命令中,命令”rpm-qa“ 的输出作为telnet的输入。连接输入输出的中间设备即为一个管道文件,因此,使用管道可以将一个命令的输出作为一命令的输入这种管道是临时的,命令执行完成后,将自动消失,这类管道成为无名管道PIPE。
PIPE 和普通文件有着很大的区别,首先在进程通信的两端退出后,管道将自动消失并释放内核资源,不能像普通文件那样存储大量的信息。
包含头文件<unistd.h>
功能:创建一无名管道
原型:
int pipe(int fd[2]);
参数:
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
man帮助说明:
pipe() creates a pipe, a unidirectional data channel that can be used
for interprocess communication. The array pipefd is used to return
two file descriptors referring to the ends of the pipe. pipefd[0]
refers to the read end of the pipe. pipefd[1] refers to the write
end of the pipe. Data written to the write end of the pipe is
buffered by the kernel until it is read from the read end of the
pipe. For further details, see pipe(7).
任何进程读写PIPE都要确定还有一个进程(这个进程可以是自己),该进程以写读的方式(读对应写,写对应读)访问管道(即可以操作相应的文件描述符)。读写管道使用的系统调用是write和read,两者都默认以阻塞的方式读写管道,如果要修改这两个函数的行为可以通过fcntl函数实现。
用 pipe() 创建的管道两端处于同一个进程中,由于管道主要是用于在不同的进程间通信的,因此,在实际应用中没有太大意义。实际上,通常先是创建一个管道,再调用fork()函数创建一个子进程,该子进程会继承父进程所创建的管道,这时,父子进程管道的文件描述符对应关系如下图
此时的关系看似非常复杂,实际上却已经给不同进程之间的读写创造了很好的条件。父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭即可。例如,图4中,将父进程的写端fd[1]和子进程的读端fd[0]关闭,则父子进程之间就建立起一条“子进程写入父进程读取”的通道。 同样,也可以将父进程的读端fd[0]和子进程的写端fd[1]关闭,则父子进程之间就建立起一条“父进程写入子进程读取”的通道
另外,父进程还可以创建多个子进程,各个子进程都继承了相应的fd[0]和fd[1],此时,只需要关闭相应的端口就可以建立各子进程之间的的通道。
管道读写注意点
● 只有在管道的读端存在时,向管道写入数据才有意义。否则,向管道写入数据的进程将收到内核传来的 SIGPIPE 信号(通常为 Broken pipe错误)。
● 向管道写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写进程将会一直阻塞。
● 父子进程在运行时,它们的先后次序并不能保证。因此,为了保证父子进程已经关闭了相应的文件描述符,可在两个进程中调用 sleep()函数。当然,这种调用不是很好的解决方法,以后我会用进程之间的同步与互斥机制来修改它的!
利用管道进行父子进程间数据传输
示例一:子进程向管道中写数据,父进程从管道中读出数据
/************************************************************************* > File Name: fathson.c > Author:SuooL > Mail:[email protected] > Created Time: 2014年08月08日 星期五 20时46分17秒 ************************************************************************/ #include<stdio.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> int main(void) { int fds[2]; if(pipe(fds) == -1){ perror("pipe error"); exit(EXIT_FAILURE); } pid_t pid; pid = fork(); if(pid == -1){ perror("fork error"); exit(EXIT_FAILURE); } if(pid == 0){ close(fds[0]);//子进程关闭读端 write(fds[1],"hello",5); exit(EXIT_SUCCESS); } close(fds[1]);//父进程关闭写端 char buf[10] = {0}; read(fds[0],buf,10); printf("receive datas = %s\n",buf); return 0; }
示例二:利用管道实现ls |wc –w功能
/************************************************************************* > File Name: ls-wc.c > Author:SuooL > Mail:[email protected] > Created Time: 2014年08月08日 星期五 20时53分04秒 > Description: 使用管道实现ls| wc -w命令 ************************************************************************/ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> int main(void) { int fds[2]; if(pipe(fds) == -1){ perror("pipe error"); exit(EXIT_FAILURE); } pid_t pid; pid = fork(); if(pid == -1){ perror("fork error"); exit(EXIT_FAILURE); } if(pid == 0){ dup2(fds[1],STDOUT_FILENO);//复制文件描述符且指定新复制的fd为标准输出 close(fds[0]);//子进程关闭读端 close(fds[1]); execlp("ls","ls",NULL); fprintf(stderr,"exec error\n"); exit(EXIT_FAILURE); } dup2(fds[0],STDIN_FILENO); close(fds[1]);//父进程关闭写端 close(fds[0]); execlp("wc","wc","-w",NULL); fprintf(stderr, "error execute wc\n"); exit(EXIT_FAILURE); }
1、shell重定向操作
2、重定向编程
一般的输入输出函数都会默认向指定的文教描述符的文件读写。因此,重定向操作实际上是关闭某个标准I/O 设备(文件描述符0,1,2),而将另外一个打开的普通文件描述符设置为0,1,2.
输入重定向:关闭标准输入设备,打开或复制普通文件,将其文件描述符设置为0,
输出重定向,错误输出重定向同理。
使用dup和dup2函数可以实现文件描述符的复制操作。
函数具体的说明参见网络说明文档。
下面是一个使用dup2函数将输出重定向到某文件的示例:
/************************************************************************* > File Name: dup_exp.c > Author:SuooL > Mail:[email protected] > Created Time: 2014年08月08日 星期五 21时15分31秒 > Description: 使用dup2实现输出重定向 ************************************************************************/ #include <unistd.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int fd; char buffer[BUFFER_SIZE]; if(argc != 2) { fprintf(stderr,"Usage:%s utfilename/n/a\n",argv[0]); exit(EXIT_FAILURE); } // 打开重定向文件 if((fd=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR)) == -1) { fprintf(stderr,"Open %s Error:%s/n/a\n",argv[1],strerror(errno)); exit(EXIT_FAILURE); } if(dup2(fd,fileno(stdout)) == -1) // 重定向设备 { fprintf(stderr,"Redirect standard out error:%s/n/a\n",strerror(errno)); exit(EXIT_FAILURE); } fprintf(stderr,"Now,please input string\n"); fprintf(stderr,"(to quit use CTRL+D)/n\n"); while(1) { fgets(buffer,BUFFER_SIZE,stdin); // 从标准输入读取 if(feof(stdin)) break; // 写数据,重定向到文件 write(fileno(stdout),buffer,strlen(buffer)); } exit(EXIT_SUCCESS); return 0; }
即是使用无名管道将执行who命令的进程与执行sort命令的进程联系一起,将登陆的用户的信息按排序输出。
需要:
系统创建一个PIPE,在执行who命令的进程将输出重定向到PIPE的写端,而在执行sort命令的进程将输入重定向到管道的读端,即是用管道将sort的输入连接到who的输出。
步骤:
1、主进程创建一个管道,显然主进程可以访问管道的两端。
2、主进程创建两个子进程分别执行who和sort命令。两个子进程都继承了父进程打开的文件描述符。
3、关闭每个进程与管道无关联系。
4、在两个子进程中调用execX 函数,执行who和sort命令。代码如下:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/wait.h> #include <stdlib.h> int main(int argc,char *argv[]) { int fds[2]; if(pipe(fds)==-1) { perror("pipe"); exit(EXIT_FAILURE); } if (fork()== 0) { char buf[128]; dup2(fds[0], 0); close(fds[1]); //must include ,or block execlp("sort", "sort", (char *)0); //execlp("cat", "cat", (char *)0); } else { if(fork() == 0) { dup2(fds[1], 1); close(fds[0]); execlp("who", "who", (char *)0); } else { close(fds[0]); close(fds[1]); wait(NULL); wait(NULL); } } return 0; }
管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。
值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
它们不支持诸如lseek()等文件定位操作。
使用mkfifo()函数来创建
包含头文件:
#include <sys/types.h>
#include <sys/stat.h>
函数原型:
int mkfifo(const char * pathname, mode_t mode)
参数:
pathname是一个普通的路径名,也就是创建后FIFO的名字;
mode指出FIFO文件的操作权限,与打开普通文件的open()函数中的mode 参数相同。
常用的mode:
O_CREAT:如果文件不存在,则创建之;
O_EXCL:确保此次调用会创建文件;
如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。
返回值:
创建成功,则返回0;
失败则返回-1;
错误类型:
EEXIST 路径名已存在(最常用的判断)
ENAMETOOLONG/ ENOENT/ENOSPC/ ENOTDIR/ EROFS
有名管道比管道多了一个打开操作:open(),以供读取或者写入
包含头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数:
1> pathname:指出文件名
2> flags:至少包含一下一种访问权限:O_RDONLY ( read only ), O_WRONLY( write only ), or O_RDWR( read/write ).
falgs需要与一些文件状态标识进行“ 或 ”操作
FIFO常用的文件状态标识:O_NONBLOCK or O_NDELAY
即表示非阻塞(如果不设置,则默认为阻塞),含义看下文解释。
3> mode:一般取0即可;
返回值:
如果打开成功,则返回一个新的文件描述符;
如果打开失败,则返回-1,并设置响应的errno
FIFO常用到的错误:
ENXIO:当设置flags为O_NONBLOCK | O_WRONLY时,文件为FIFO,并且进程没有文件打开以供写;
FIFO的打开规则:
如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
对FIFO打开规则的验证(主要验证写打开对读打开的依赖性)
从FIFO中读取数据:
约定:
如果一个进程为了从FIFO中读取数据而阻塞打开FIFO(open()时不设置O_NONBLOCK ),那么称该进程内的读操作为设置了阻塞标志的读操作。
如果有进程写打开FIFO,且当前FIFO内没有数据
1> 则对于设置了阻塞标志的读操作来说,将一直阻塞
2> 对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:
1> 当前FIFO内有数据,但有其它进程在读这些数据;
2> FIFO内没有数据。
解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
使用read()方法从FIFO中读取数据
包含头文件:
#include <unistd.h>
函数原型:
ssize_t read(int fd, void *buf, size_t count);
功能:
从文件描述符fd中读取count字节到buf中;
参数:
如果count==0,则函数返回0;如果count大于buf的size,结果不确定??
返回值:
如果读取成功,则返回读取的字节数;
(当申请的字节数多余fd中可读的字节数时,返回的值小于count)
如果读取失败,则返回-1;
FIFO中可能出现的错误:
EAGAIN or EWOULDBLOCK:The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.
向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO(open()时不设置O_NONBLOCK ),那么称该进程内的写操作为设置了阻塞标志的写操作。
对于设置了阻塞标志的写操作:
1> 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
2> 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
1> 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。(不保证所有数据都能写入)
2> 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;
如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
要注意几种不同情况下的区别!
使用write()方法向FIFO写入数据;
包含头文件:
#include <unistd.h>
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
返回值:
如果成功,则返回写入的字节数;
如果失败,返回-1,并设置相应的errno;
可能出现的错误:
EAGAIN or EWOULDBLOCK:The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the write would block.
对FIFO读写规则的验证:
下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。
程序1:写FIFO的程序
程序应用说明:
把读程序编译成两个不同版本:
1>阻塞读版本
2>非阻塞读版本nbr
把写程序编译成两个四个版本:
1> 非阻塞且请求写的字节数大于PIPE_BUF版本:nbwg
2> 非阻塞且请求写的字节数不大于PIPE_BUF版本:版本nbw
3> 阻塞且请求写的字节数大于PIPE_BUF版本:bwg
4> 阻塞且请求写的字节数不大于PIPE_BUF版本:版本bw
FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。
管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输"协议",采用传播具有特定意义的消息。
要灵活应用管道及FIFO,理解它们的读写规则是关键。
1、管道是特殊类型的文件,在满足FIFO的原则条件下可能进行读写,但不能定位读写位置。
2、管道单向,要实现双向则需要俩个管道。无名管道一般用于亲缘关系的进程间通信,而有名管道则以磁盘文件的形式存在,可以实现本机任意两个进程间的通信
3、阻塞问题。无名管道必须显示打开,创建的时候直接返回文件描述符,而在读写的时候需要确定对方的存在,即阻塞于读写位置,而有名管道则在打开的时候确定对方是否存在,否则阻塞。(关于此会具体讲述)
进程异步信号处理机制
System V 进程间通信
转载请注明出处: http://blog.csdn.net/suool/article/details/38444149, 谢谢!