数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
UNIX 进程间通信(IPC)方式包括管道、FIFO、信号。
Linux 中使用较多的进程间通信方式主要有以下几种。
(1)管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
(2)信号(Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知接受进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。
(3)消息队列:消息队列是消息的链接表,包括Posix 消息队列systemV 消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。
(4)共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
(5)信号量:主要作为进程间以及同一进程不同线程之间的同步手段。
(6)套接字(Socket):这是一种更为一般的进程间通信机制,它可用于不同机器之间的进程间通信,应用非常广泛。
[b]管道通信[/b]
普通的Linux shell都允许重定向,而重定向使用的就是管道。例如:
ps | grep vsftpd
管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
管道主要用于不同进程间通信。
创建一个简单的管道,可以使用系统调用pipe( )。它接受一个参数,也就是一个包括两个整数的数组。如果系统调用成功,此数组将包括管道使用的两个文件描述符。创建一个管道之后,一般情况下进程将产生一个新的进程。
系统调用:pipe( );
原型:int pipe( int fd[2] );
返回值:如果系统调用成功,返回0。如果系统调用失败返回- 1:
errno = EMFILE (没有空闲的文件描述符)
EMFILE (系统文件表已满)
EFAULT (fd数组无效)
注意:fd[0] 用于读取管道,fd[1] 用于写入管道。
#i nclude <unistd.h>
#i nclude <errno.h>
#i nclude <stdio.h>
#i nclude <stdlib.h>
int main()
{
int pipe_fd[2];
if(pipe(pipe_fd)0)
{
printf("pipe create error\n");
return -1;
}
else
printf("pipe create success\n");
close(pipe_fd[0]);
close(pipe_fd[1]);
}
管道主要用于不同进程间通信。实际上,通常先创建一个管道,再通过fork函数创建一个子进程。
可以通过打开两个管道来创建一个双向的管道。但需要在子进程中正确地设置文件描述符。
必须在系统调用fork( )中调用pipe( ),否则子进程将不会继承文件描述符。
当使用半双工管道时,任何关联的进程都必须共享一个相关的祖先进程。因为管道存在于系统内核之中,所以任何不在创建管道的进程的祖先进程之中的进程都将无法寻址它。而在命名管道中却不是这样。
与linux中文件操作有文件流的标准I/O一样,管道的操作也支持基于文件流的模式。接口函数如下
库函数:popen();
原型: FILE *popen ( char *command, char *type);
返回值:如果成功,返回一个新的文件流。如果无法创建进程或者管道,返回NULL。
管道中数据流的方向是由第二个参数type控制的。此参数可以是r或者w,分别代表读或写。但不能同时为读和写。在Linux系统下,管道将会以参数type中第一个字符代表的方式打开。所以,如果你在参数type中写入rw,管道将会以读的方式打开。
使用popen()创建的管道必须使用pclose( )关闭。其实,popen/pclose和标准文件输入/输出流中的fopen() / fclose()十分相似。
库函数: pclose();
原型: int pclose( FILE *stream );
返回值: 返回系统调用wait4( )的状态。
如果stream无效,或者系统调用wait4( )失败,则返回 -1。
注意此库函数等待管道进程运行结束,然后关闭文件流。
库函数pclose( )在使用popen( )创建的进程上执行wait4( )函数。当它返回时,它将破坏管道和文件系统。
#i nclude <stdio.h>
#i nclude <unistd.h>
#i nclude <stdlib.h>
#i nclude <fcntl.h>
#define BUFSIZE 1024
int main()
{
FILE *fp;
char *cmd = "ps -ef";
char buf[BUFSIZE];
buf[BUFSIZE] = '\0';
if((fp=popen(cmd,"r"))==NULL)
perror("popen");
while((fgets(buf,BUFSIZE,fp))!=NULL)
printf("%s",buf);
pclose(fp);
exit(0);
}
[b]命名管道([/b][b]FIFO[/b][b])[/b]
命名管道和一般的管道基本相同,但也有一些显著的不同:
n 命名管道是在文件系统中作为一个特殊的设备文件而存在的。
n 不同祖先的进程之间可以通过管道共享数据。
n 当共享管道的进程执行完所有的I / O操作以后,命名管道将继续保存在文件系统中以便以后使用。
管道只能由相关进程使用,它们共同的祖先进程创建了管道。但是,通过FIFO,不相关的进程也能交换数据。
命名管道创建
#i nclude
#i nclude
int mkfifo(const char * pathname,
mode_t mode) ;
返回:若成功则为0,若出错则为- 1
一旦已经用mkfifo创建了一个FIFO,就可用open打开它。确实,一般的文件I / O函数(close、read、write、unlink等)都可用于FIFO。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
(1) 在一般情况中(没有说明O_NONBLOCK),只读打开要阻塞到某个其他进程为写打开此FIFO。类似,为写而打开一个FIFO要阻塞到某个其他进程为读而打开它。
(2) 如果指定了O_NONBLOCK,则只读打开立即返回。但是,如果没有进程已经为读而打开一个FIFO,那么只写打开将出错返回,其errno是ENXIO。
类似于管道,若写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志。
FIFO相关出错信息:
n EACCES (无存取权限)
n EEXIST (指定文件不存在)
n ENAMETOOLONG (路径名太长)
n ENOENT (包含的目录不存在)
n ENOSPC (文件系统剩余空间不足)
n ENOTDIR (文件路径无效)
n EROFS (指定的文件存在于只读文件系统中)
[b]信号通信[/b]
信号是软件中断。信号(signal)机制是Unix系统中最为古老的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。
很多条件可以产生一个信号。
n 当用户按某些终端键时,产生信号。在终端上按DELETE键通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。(第11章将说明此信号可被映射为终端上的任一字符。)
n 硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无效存储访问的进程产生一个SIGSEGV。
n 进程用kill( 2 )函数可将信号发送给另一个进程或进程组。自然,有些限制:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
n 用户可用kill( 1 )命令将信号发送给其他进程。此程序是kill函数的界面。常用此命令终止一个失控的后台进程。
n 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里并不是指硬件产生条件(如被0除),而是软件条件。例如SIGURG (在网络连接上传来非规定波特率的数据)、SIGPIPE (在管道的读进程已终止后一个进程写此管道),以及SIGALRM(进程所设置的闹钟时间已经超时)。
内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下:
n 异常:进程运行过程中出现异常;
n 其它进程:一个进程可以向另一个或一组进程发送信号;
n 终端中断:Ctrl-C,Ctrl-\等;
n 作业控制:前台、后台进程的管理;
n 分配额:CPU超时或文件大小突破限制;
n 通知:通知进程某事件发生,如I/O就绪等;
n 报警:计时器到期。
下面是几个常见的信号。
n SIGHUP: 从终端上发出的结束信号;
n SIGINT: 来自键盘的中断信号(Ctrl-C);
n SIGQUIT:来自键盘的退出信号(Ctrl-\);
n SIGFPE: 浮点异常信号(例如浮点运算溢出);
n SIGKILL:该信号结束接收信号的进程;
n SIGALRM:进程的定时器到期时,发送该信号;
n SIGTERM:kill 命令发出的信号;
n SIGCHLD:标识子进程停止或结束的信号;
n SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。
(1) 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是未定义的。
(2) 捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。
(3) 执行系统默认动作。对大多数信号的系统默认动作是终止该进程。
每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:
n 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
n 退出(exit):不产生core文件,直接终止进程。
n 忽略(ignore):忽略该信号。
n 停止(stop):挂起该进程。
n 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。
[b]信号发送与捕捉[/b]
kill()和raise()
kill()不仅可以中止进程,也可以向进程发送其他信号。
与kill函数不同的是,raise()函数运行向进程自身发送信号。
#i nclude
#i nclude
int kill(pid_t pid, int signo) ;
int raise(int signo) ;
两个函数返回:若成功则为0,若出错则为-1。
kill的pid参数有四种不同的情况:
n pid>0 将信号发送给进程ID为pid的进程。
n pid == 0 将信号发送给其进程组I D等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程。
n pid 将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,“所有进程”并不包括系统进程集中的进程。
n pid ==-1 POSIX.1未定义此种情况。
#i nclude <stdio.h>
#i nclude <stdlib.h>
#i nclude <signal.h>
#i nclude <sys/types.h>
#i nclude <sys/wait.h>
int main()
{
pid_t pid;
int ret;
if((pid=fork())0){
perror("fork");
exit(1);
}
if(pid == 0){
raise(SIGSTOP);
exit(0);
}
else{
printf("pid=%d\n",pid);
if((waitpid(pid,NULL,WNOHANG))==0){
if((ret=kill(pid,SIGKILL))==0)
printf("kill %d\n",pid);
else{
perror("kill");
}
}
}
}
使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程。
#i nclude
unsigned int alarm(unsigned int seconds) ;
返回:0或以前设置的闹钟时间的余留秒数
参数seconds的值是秒数,经过了指定的seconds秒后会产生信号SIGALRM。
每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前登记的闹钟时间则被新值代换。
如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。
pause函数使调用进程挂起直至捕捉到一个信号。
#i nclude
int pause(void);
返回:-1,errno设置为EINTR
只有执行了一个信号处理程序并从其返回时,pause才返回。
#i nclude <unistd.h>
#i nclude <stdio.h>
#i nclude <stdlib.h>
int main()
{
int ret;
ret=alarm(5);
pause();
printf("I have been waken up.\n",ret);
}