1、通信
通信就是交互,就是相互之间传递信息,这是人的基本需求。在现代社会里,一个人如果不与他人交互就无法生存。进程作为人的发明,自然脱离不了人的习性,也有通信需求。如果进程之间不进行任何通信,那么进程所能完成的任务就要大打折扣。合作进程之间需要通信,父子进程之间也需要通信。而线程间的通信则需要更多。由于一个进程通常包括多个线程,这多个线程之间因资源共享自然地就存在一种合作关系。这种合作关系虽然可以表现为相互独立,但更多的时候是互相交互,这就是通信。
从多线程模式角度,由于进程的执行实体是线程,而每个进程至少有一个线程,进程通信不仅包括不同进程的线程之间的通信,更多的是同一进程内的多个线程之间的通信。所以,进程通信的实质乃是线程通信。
进程通信的规范化首先是由UNIX引出的,UNIX将其称之为IPC(Inter-Process Communication进程间通信)。对UNIX发展作出重大贡献的两大主力——AT&T的贝尔实验室和加州大学伯克利分校,在IPC方面的研究侧重点有所不同,前者形成的System V IPC标准重点在本地通信;而后者形成的BSD IPC标准则侧重于远程通信。此后,由于UNIX版本的多样性,为了统一标准,IEEE(电子电气工程师协会)开发了一个独立的UNIX标准——ANSI UNIX,并称为POSIX(计算机环境可移植性操作系统界面)。现有的大部分UNIX流行版本都遵循了POSIX标准。
POSIXIPC主要包括以下几种通信机制:
■管道(pipe)。这是利用共享文件实现进程间通信的机制,包括匿名管道和命名管道。前者可用于具有亲缘关系的线程间的通信;后者除了具有匿名管道的功能外,则还允许无亲缘关系的线程间的通信。
■信号(signal)。信号是一种通知性的通信方式。当某种事件发生时用于向接收者发出通知,接收者可根据需要做出某种响应,也可以不予理睬。
■信号量(semaphore)。信号量较之信号更为可靠和有效,但其主要是作为任意线程之间的同步工具。
■共享内存。共享内存是多个线程可以共同访问的一块内存空间,是最快的通信方式。但它不具备同步机制,所以往往需要编程人员使用信号量或锁与它配合。
■套接字(socket)。主要用来实现网络上不同节点上的进程之间的通信。
■消息队列。消息(message)是一种有结构的数据,消息队列是由多个消息组成的链表。与信号相比,消息队列能承载更大的信息量;与管道相比,消息队列克服了管道只能承载无格式字节流以及缓冲区大小受限的缺点。所以消息队列是一种比较完备的通信机制。
2、匿名管道通信
管道(pipe)是UNIX早期的一个重要通信机制。其思想是在内存中创建一个共享文件,使通信双方利用这个共享文件来传递信息。由于这种通信方式具有数据只能从写入端写入且只能从读出端读出的单向传送数据的特点,所以这个作为传递信息的共享文件就称之为“管道”。管道有匿名管道和命名管道之分。
匿名管道具有以下特点:
①匿名管道是半双工的,数据只能向一个方向流动;要求双向通信时,需要建立两个匿名管道。
②匿名管道只能用于具有亲缘关系的线程之间的通信,如同一进程内的线程之间、父进程内的线程与子进程内的线程之间、兄弟进程内的线程之间。
③匿名管道对于管道两端的通信者而言,就是一个文件,但它不是普通文件,而是一个只存在于内存中的特殊文件,实际上是一片共享的内存缓冲区。
④一个写者向管道中写入的数据被管道另一端的读者读出。写入的数据每次都添加在管道的末尾,并且每次都是从管道的头部读出数据。
匿名管道可以在shell命令行中创建也可以在在程序中创建。
在shell命令行中,只需要使用符号“|”即可。如,在Linux的shell界面上,我们可以键入如下命令行:
$sort file1 | grep feng
该命令行在“sort”(排序)和“grep”(查找)这两个utility之间建立了一个匿名管道,数据从sort流向grep,sort的结果将作为grep的输入。其语义是sort程序对当前目录下的文件file1进行排序,排序结果作为grep程序的输入,从排序结果中找出所有包含有字符串feng的文本行。
在程序里面,可使用库函数pipe()创建匿名管道,pipe的原型如下: int pipe( int fd[2] );
若创建成功,pipe函数的返回值为0,否则为-1。成功返回时,数组fd中将被填入两个有效的文件描述符(文件在打开文件表中的索引号 ),元素fd[0]是一个具有“只读”属性的文件描述符,而元素fd[1]则是一个具有“只写”属性的文件描述符,即fd[0]和fd[1]分别表示管道的读端与写端,通过fd[0]只能对管道进行读,而通过fd[1]只能对管道进行写。
【说明】
(1)库函数pipe()由编译器将它扩展为相应的pipe系统调用。(在头文件unistd.h中声明)
(2)文件描述符(file descriptor)是UNIX的术语,是一个已打开文件在打开文件表中的索引号,一般来说,每个进程可以打开64个文件。在Windows中称文件描述符为文件句柄(handle)。一般文件的I/O库函数都可以用于管道,如read()、write()和close()等。例如,下述C语言代码段创建一个匿名管道并利用它在父子进程间通信。
实现父子进程之间的管道通信。程序中由父进程创建一个匿名管道,并创建了2个子进程。两个子进程都从管道写端(入口)写入一个发给父进程的消息(字符串),父进程则从管道读端(出口)读取发来的两个消息。
2.1下面是程序的代码:
/*test5_1.c:匿名管道*/
# include <stdio.h>
# include <unistd.h>
#include<stdlib.h>
#include<sys/wait.h> //<缺少的头文件>
int main( )
{
int i ,p1 ,p2 , pid , fd[2] ;
char buf[80];
pipe(fd); //<创建一个匿名管道>
while((p1=fork( ))==-1);
if (p1==0) { /*子进程1代码*/
sprintf (buf,”Message of child_1”); /*在buf中写入消息文本*/
lockf(fd[0],1,0); //在读写管道的过程中,一般采用lockf()函数讲文件锁定
write(fd[1],buf,80); //fd[1]是一个具有“只写”属性的文件描述符
lockf(fd[0],0,0); //读写结束,解锁
printf (“Child_1 write an message to pipe!\n”);
for( i=0; i<99999; i++);
exit(0); //<终止>
}
else{
while((p2=fork())==-1);
if (p2==0){ /*子进程2代码*/
sprintf (buf,”Message of child_2”);
//在读写管道的过程中,一般采用lockf()函数讲文件锁定
lockf(fd[1],1,0);
write(fd[1],buf,80); //fd[1]是一个具有“只写”属性的文件描述符
lockf(fd[1],0,0); //读写结束,解锁
printf (“Child_2 write an message to pipe!\n”);
for( i=0; i<99999; i++);
exit(0); //<终止>
}
else { /*父进程代码*/
for(i=0;i<2;i++)
{
pid=wait(NULL);
read(fd[0],buf,80);
if(pid==p1)
printf("parent read an message of child_1%s\n",buf);
else
printf("parent read an message of child_2%s\n",buf);
}
/*fd[0]是一个具有“只读”属性的文件描述符,等待子进程并通过PID读出两个子进程采用wait()函数,wait()函数可以返回结束的进程号。采用close()函数,先关闭匿名管道的写入端,再关闭读出端并输出提 示符。 */
close(fd[0]);
close(fd[1]); //<关闭管道>
printf(“OVER\n”);
}
}
return 0;
}
2.2运行结果截图
3、信号通信
信号(Signal)也称软中断,其实质是在软件层次上对硬件中断机制的一种模拟,一个进程收到一个信号与CPU收到一个中断请求可以说是一样的,即信号的到来意味着进程可能要中断现行工作而去执行特定的信号处理代码。
信号是种通知性通信机制,即信号只是用来通知某进程发生了什么事件,并不给该进程传递数据。产生信号的通常是一些特定的事件,操作系统定义了这些事件和对应的信号(整数值)及其默认的处理方法。用户进程也可以自定义信号及其处理方法。
信号机制支持进程之间传递同步信号,也支持用户通过键盘按键向进程传递控制信号。在进程的PCB中设置有
一个信号队列,与中断不同,信号没有优先级,按FIFO排队并处理。进程对信号的捕获与响应通常发生在系统调用或中断处理的末尾处,也就是说在系统调用程序或中断处理程序的尾部都安排有信号的捕获和响应的指令,一旦发现有发送给该进程需要处理的信号,就立即转去执行相应的信号处理代码,完成后再返回进程。
进程对信号可以有三种不同的处理方法:
(1)捕获信号:该方法类似于中断的处理程序,用户可以在进程中为需要处理的信号绑定自定义的信号处理代码,一旦该信号发生,便执行相应的信号处理代码,而不执行系统定义的默认操作。
(2)执行默认操作:如果对某信号未绑定用户自定义
的信号处理代码,则一旦该信号发生,便自动执行系统定义的默认操作。对大多数信号而言,操作系统默认的操作都是终止该进程。
(3)忽略信号:进程可指定忽略某个信号,即对该信号不做任何处理,就像未发生过一样。但有些信号是不能忽略的,如Linux定义的SIGKILL信号和SIGSTOP信号,因为它们为超级用户提供了一种使进程终止或停止的可靠方法。
Linux的信号机制基本上是继承UNIX的,但也作了些扩充。可通过输入shell命令“kill -l”获取Linux支持的信号列表。表3-1列出了Linux支持的部分信号的编号、名称、默认操作及对应的事件。Linux支持一组信号处理库函数,包括信号安装函数和信号发送函数等,它们都被定义在头文件signal.h中。
signal()的原型是:
void signal(int sig, void(*sigfun)(int));
其中,
参数sig是信号值(信号编号或信号名);
sigfun是信号处理函数的指针,该函数的形参是信号值(可缺省)。
sigfun也可以是符号常量SIG_DEL和SIG_IGN,
SIG_DEL表示执行系统的默认操作;
SIG_IGN表示忽略该信号。
下面给出signal()的应用示例。实现父子进程之间的信号通信。在创建了两个子进程后,父进程在接收到外部中断信号SIGINT后再启动其后续工作。而两个子进程则在分别接收到父进程发来的SIGSUR1信号和SIGSUR2信号后再启动它们的后续工作。
3.1程序代码
/*test5_2.c:信号通信*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h> //<包含相关头文件>
int wait_flag=1; /*同步标志*/
void start( ) /*自定义的信号处理函数*/
{ wait_flag=0; }
int main()
{ int p1,p2;
long int i;
signal(SIGINT,start); // <安装信号:中断信号SIGINT与start函数绑定>
printf(“Parent create child!\n”);
while((p1=fork())= =-1); /*创建子进程1*/
if (p1>0)
{
while((p2=fork())= =-1); /*创建子进程2*/
if (p2>0){
printf (“Enter interrupt signal!\n”);
while(wait_flag!=0);
kill(p1,SIGUSR1); //<向子进程1发送SIGUSR1信号>
kill(p1,SIGUSR1); //<向子进程2发送SIGUSR2信号>
wait();
wait(); //<等待子进程终止>
printf(“Over!\n”);
}
else{ /*子进程2代码*/
printf(“Child_2 is running.\n”);
wait_flag=1;
signal(SIGINT,start); // <安装信号:中断信号SIGINT与start函数绑定>
while(wait_flag!=0);
for(i=0;i<999999;i++);
printf (“Child_2 is over!\n”);
exit(0); //<终止>
}
}
else{ /*子进程1代码*/
printf(“Child_1 is running.\n”);
wait_flag=1;
signal(SIGINT,start); // <安装信号:中断信号SIGINT与start函数绑 定>
while(wait_flag!=0);
for(i=0;i<999999;i++);
printf (“Child_1 is over!\n”);
exit(0); //<终止>
}
return 0;
}
3.2信号通信截图