操作系统之进程通信
引子 进程通信的方式
△信号通信
△管道通信
△消息队列
△共享存储区
一、信号通信
1.什么是信号
(1)信号是Linux进程之间一种重要的通信机制;
(2)信号的作用是为了通知进程某个事件已经发生;
(3)信号的发出是及时的,但是信号的响应可能会有延后,收到信号的进程在当前执行处设置断点,然后立即转为执行信号处理函数,执行结束后,会回到断点,继续执行之前的操作,这一点类似中断机制;
(4)信号机制其实是在软件层次上对中断机制的一种模拟,一个进程收到信号和收到中断请求可以说是一样的;
(5)中断和信号的区别是,前者运行在核心态(系统),后者运行在用户态,中断的响应比较及时,而信号的相应一般会有延迟;
(6)信号的发出者可以是进程、系统、硬件。
2.Linux下的信号
在终端输入指令“kill -l”可以查看62个信号(没有编号32和33)。SIGUSR1和SIGUSR2是用户可以自定义的信号,较为常用。
3.Linux下使用信号机制
(1)“ctrl+c”杀死一个进程:摁下“ctrl+c”会产生信号SIGINT,进程接收到SIGINT信号后,会结束进程。
(2)“ctrl+z”挂起一个进程:摁下“ctrl+c”会产生信号SIGSTP,进程接收到SIGSTP信号后,会挂起进程。
(3)“kill -9”杀死一个进程:在终端输入“kill -9”后回车,会产生信号SIGKILL,进程收到SIGKILL信号后,会强制结束进程。
4.signal()函数
signal()函数的作用是为指定的信号注册处理函数,函数格式是
sighandler_t signal(int signum, sighandler_t handler);
sighandler的定义是
typedef void (*sighandler_t)(int);
参数signum是指定信号的标号,handler是处理函数的函数名。
注意:
①当handler=1时,进程将忽略(屏蔽)signum所示的信号,不会对信号做出响应;
②当handler=0(默认值)时,进程在收到signum所示的信号后会立即终止自己,类似于“ctrl+c”;
③当handler为大于1的正整数,即一个函数名称时,进程在接收到signum所示的函数后会执行响应的函数。
5.kill()函数
kill()函数的作用是向指定的进程发送信号,函数格式是
int kill(int pid, int sig);
参数pid是进程号,sig是要发送的软中断信号。
6.一个信号通信的实例
编写一段代码,创建一个子进程。程序开始运行时,处于阻塞等待状态。在键盘上摁下“ctrl+c”后,父进程打印“Parent process:Transmitted signal to my subprocess”,然后子进程打印“Subprocess:Got the signal from my parent process”,然后退出程序。
1 //文件名称为test2.c 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <signal.h> 6 7 int waitFlag = 0; 8 9 void stopWaiting(); 10 void waitForSignal(); 11 12 int main() 13 { 14 int pid; //子进程ID号 15 16 pid = fork(); //创建子进程 17 if(pid == -1) //进程创建失败 18 { 19 exit(1); 20 } 21 if(pid != 0) //父进程中执行 22 { 23 signal(SIGINT, stopWaiting); //为SIGINT信号重新注册处理函数 24 waitForSignal(); //进入等待函数,将父进程阻塞,等待SIGINT信号的到来 25 printf("Parent process:Transmitted signal to my subprocess\n"); //等待结束后,打印提示信息 26 kill(pid, SIGUSR1); //向子进程附送用户自定义信号 27 } 28 else //子进程中执行 29 { 30 signal(SIGUSR1, stopWaiting); //为SIGUSR1信号注册处理函数 31 waitForSignal(); //进入等待函数,将子进程阻塞,等待父进程发送SIGUSR1信号 32 printf("Subprocess:Got the signal from my parent process\n"); //等待结束后,打印提示信息 33 } 34 35 return 0; 36 } 37 38 void stopWaiting() 39 { 40 waitFlag = 0; //将等待标志清零 41 } 42 43 void waitForSignal() 44 { 45 waitFlag = 1; //置数等待标志 46 while(waitFlag == 1); //将程序阻塞在此处 47 }
运行结果如下:
摁下“ctrl+c”之后,仅打印了父进程提示语句,而子进程提示语句却没有打印,这是为什么呢?因为摁下“ctrl+c”后,信号SIGINT会向所有的进程发送,所以子进程也收到了SIGINT信号,但是在子进程中却没有对SIGINT函数进行重新注册,所以子进程仍然认为“ctrl+c”摁下后会退出进程。所以导致子进程的提示信息没有正常打印。我们可以在子进程中对SIGINT函数进行重新注册,比如将它忽略,这样就可以解决问题了。
新的代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <signal.h> 5 6 int waitFlag = 0; 7 8 void stopWaiting(); 9 void waitForSignal(); 10 11 int main() 12 { 13 int pid; //子进程ID号 14 15 pid = fork(); //创建子进程 16 if(pid == -1) //进程创建失败 17 { 18 exit(1); 19 } 20 if(pid != 0) //父进程中执行 21 { 22 signal(SIGINT, stopWaiting); //为SIGINT信号重新注册处理函数 23 waitForSignal(); //进入等待函数,将父进程阻塞,等待SIGINT信号的到来 24 printf("Parent process:Transmitted signal to my subprocess\n"); //等待结束后,打印提示信息 25 kill(pid, SIGUSR1); //向子进程发送用户自定义信号 26 } 27 else //子进程中执行 28 { 29 signal(SIGUSR1, stopWaiting); //为SIGUSR1信号注册处理函数 30 signal(SIGINT, SIG_IGN); //SIG_IGN就是数字1,代表忽略SIGINT信号 31 waitForSignal(); //进入等待函数,将子进程阻塞,等待父进程发送SIGUSR1信号 32 printf("Subprocess:Got the signal from my parent process\n"); //等待结束后,打印提示信息 33 } 34 35 return 0; 36 } 37 38 void stopWaiting() 39 { 40 waitFlag = 0; //将等待标志清零 41 } 42 43 void waitForSignal() 44 { 45 waitFlag = 1; //置数等待标志 46 while(waitFlag == 1); //将程序阻塞在此处 47 }
新的运行结果:
可以看到,可以正确打印父进程和子进程的提示信息了。
这段程序的执行流程是这样的:
(1)在父进程中对“ctrl+c”发出的信号SIGINT进行重新注册,让它的处理函数变为stopWaiting(),代替了原来的“中断进程”功能。然后进入等待函数,阻塞自己,等待SIGINT信号的到来;
(2)同时子进程中对用户自定义信号SIGUSR1进行注册,使其也指向处理函数stopWaiting(),然后再使用signal函数忽略“ctrl+c”发出的SIGINT信号,防止进程退出;
(3)用户摁下“ctrl+c”后,父进程和子进程都收到了SIGINT信号,但是子进程屏蔽了该信号,所以不起作用,而父进程会处理该信号;
(4)父进程接收到SIGINT信号进入函数stopWaiting(),清零等待标志位后,解除阻塞,继续向下执行,先打印提示信息,然后向子进程发送信号SIGUSR1,最后退出进程;
(5)子进程收到信号SIGUSR1后,进入stopWaiting(),清零等待标志位后,解除阻塞,继续向下执行,打印提示信息,最后退出进程。
二、匿名管道通信
1.管道(pipe)定义
管道是进程之间的一种通信机制。一个进程可以通过管道把数据传递给另外一个进程。前者向管道中写入数据,后者从管道中读出数据。
管道的数据结构图
2.管道的工作原理
(1)管道如同文件,可读可写,有读和写两个句柄;
(2)通过写写句柄来向管道中写入数据;
(3)通过读读句柄来从管道中读取数据。
(4)匿名管道通信只能用于父子或兄弟进程的通信,由父进程创建管道,并创建子进程。
3.使用管道要注意的问题
由于管道是一块共享的存储区域,所以要注意互斥使用。所以进程每次在访问管道前,都需要先检查管道是否被上锁,如果是,则等待。如果没有,则给管道上锁,然后对管道进行读写操作。操作结束后,对管道进行解锁。
4.pipe()函数
pipe()的作用是建立一个匿名管道。函数格式是
int pipe(fd);
fd的定义如下
int fd[2];
fd[0]是读句柄,fd[1]是写句柄。
5.read()函数
read()函数的作用是从指定的句柄中读出一定量的数据,送到指定区域。函数格式是
ssize_t read(int fd, const void *buf, size_t byte_num);
fd表示读句柄,buf表示读出数据要送到的区域,byte_num是要读出的字节数,返回值是成功读出的字节数。
6.write()函数
write()函数的作用是把指定区域中一定数量的数据写入到指定的句柄中。函数格式是
ssize_t write(int fd, const void *buf, size_t byte_num);
fd表示写句柄,buf表示数据来源,byte_num表示要写入的字节数,返回值是成功写入的字节数。
7.lockf()函数
lockf()函数的作用是给特定的文件上锁。函数格式是
int lockf(int fd, int cmd, off_t len);
fd表示要锁定的文件,cmd表示对文件的操作命令(“0”表示解锁,“1”表示互斥锁定区域,“2”表示测试互斥锁定区域,“3”表示测试区域),len表示要锁定或解锁的连续字节数,如果为“0”,表示从文件头到文件尾。
8.wait()函数
wait()函数的作用是立即阻塞自己,直到当前进程的某个子进程运行结束。函数格式是
pid_t wait(int *status);
其参数用来保存进程退出时的一些状态,一般设定为NULL。返回值为退出的子进程的ID号。
9.一个匿名管道通信的实例
编写一段程序,创建两个子进程,这两个子进程分别使用管道向父进程发送数据,父进程完整接收两个子进程发送的数据后打印出来。
1 #include <stdio.h> 2 #include <signal.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 6 int main() 7 { 8 int p1, p2; //两个子进程 9 int fd[2]; //读写句柄 10 char *s1 = "The 1st subprocess's data\n"; 11 char *s2 = "The 2rd subprocess's data\n"; 12 char s_read[80]; 13 pipe(fd); //建立匿名管道 14 p1 = fork(); 15 if(p1 == 0) //子进程一中执行 16 { 17 lockf(fd[1], 1, 0); //对管道的写句柄进行锁定 18 write(fd[1], s1, 26); //向写句柄写入26个字节的数据,注意这里的字节数一定要和字符串s1中的相等,否则会在写入后增写一个结束符,导致输出不了理想的结果 19 lockf(fd[1], 0, 0); //解锁写句柄 20 exit(0); 21 } 22 else 23 { 24 p2 = fork(); 25 if(p2 == 0) //子进程二中执行 26 { 27 lockf(fd[1], 1, 0); //锁定写句柄 28 write(fd[1],s2, 26); //向写句柄写入26个字节的数据 29 lockf(fd[1], 0, 0); //解锁写句柄 30 exit(0); 31 } 32 else //父进程中执行 33 { 34 wait(NULL); //进程同步,等待一个子进程结束 35 wait(NULL); //进程同步,再等待一个子进程结束 36 //这两个等待语句是为了确保两个子进程都向管道中写入了数据后,父进程才开始读取管道中数据 37 read(fd[0], s_read, 52); //读读句柄,将读取的数据存入s_read中 38 printf("%s", s_read); //打印数据 39 exit(0); 40 } 41 } 42 43 return 0; 44 }
运行结果:
三、消息队列
1.概述
(1)消息是一个格式化的可变长的信息单元;
(2)消息通信机制允许一个进程给其他任意一个进程发送消息;
(3)当出现了多个消息时,会形成消息队列,每个消息队列都有一个关键字key,由用户指定,作用与文件描述符相当。
2.为什么引入消息队列机制
信号量和PV操作可以实现进程的同步和互斥,但是这种低级通信方式并不方便,而且局限性较大。当不同进程之间需要交换更大量的信息时,甚至是不同机器之间的不同进程需要进行通信时,就需要引入更高级的通信方式——消息队列机制。
3.信箱
消息队列的难点在于,发送方不能直接将要发送的数据复制进接收方的存储区,这时就需要开辟一个共享存储区域,可供双方对这个存储区进行读写操作。这个共享区域就叫做信箱。每个信箱都有一个特殊的标识符。每个信箱都有自己特定的信箱容量、消息格式等。信箱被分为若干个分区,一个分区存放一条消息。
4.重要的两条原语:
原语具有不可分割性,执行过程不允许被中断。
(1)发送消息原语(send):如果信箱就绪(信箱还未存满),则向当前信箱指针指向的分区存入一条消息,否则返回状态信息(非阻塞式)或者等待信箱就绪(阻塞式)。
(2)接收消息原语(receive):如果信箱就绪(信箱中有消息),则从当前信箱指针指向的分区读取一条消息,否则返回状态信息(非阻塞式)或者等待信箱就绪(阻塞式)。
注:在信箱非空的情况下,每读取一次信箱,信箱中的消息就会少一条,直到信箱变为空状态。
5.消息通信的原理
(1)如果一个进程要和另外一个进行通信,则这两个进程需要开辟一个共享存储区(信箱);
(2)消息通信机制也可以用在一对多通信上,一个server和n个client通信时,那么server就和这n个client各建立一个共享存储区;
(3)一个进程可以随时向信箱中存储消息,当然一个进程也可以随时从信箱中读取一条消息。
6.消息机制的同步作用
采用消息队列通信机制,可以实现进程间的同步操作。在介绍同步功能之前,需要先介绍两个名词,阻塞式原语和非阻塞式原语。阻塞式原语是指某进程执行一个指令时,如果当前环境不满足执行条件,则该进程会在此停止,等待系统环境满足执行条件,然后继续向下执行。非阻塞式原语是指某进程执行一个指令时,如果当前环境不满足执行条件,则立即返回一个状态信息,并继续执行接下来的指令。
(1)非阻塞式发送方+阻塞式接收方:两个进程开始运行后,接收方会进入等待状态,等待发送方给接收方发送一条消息,直到接收到相应的消息后,接收方进程才会继续向下执行。
(2)非阻塞式发送方+非阻塞式接收方:发送方和接收方共享一个信箱,发送方随时可以向信箱中存入一条消息,接收方可以随时从信箱读取一条消息。当信箱满时,发送方进入阻塞状态;当信箱空时,接收方进入阻塞状态。
7.msgget()函数
msgget()函数的作用是创建一个新的或打开一个已经存在的消息队列,此消息队列与key相对应。函数格式为
int msgget(key_t key, int msgflag);
参数key是用户指定的消息队列的名称;参数flag是消息队列状态标志,其可能的值有:IPC_CREAT(创建新的消息队列)、IPC_EXCL(与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误)、 IPC_NOWAIT(读写消息队列要求无法满足时,不阻塞);返回值是创建的消息队列标识符,如果创建失败则则返回-1。函数调用方法是:
msgget(key,IPC_CREAT|0777);
0777是存取控制符,表示任意用户可读、可写、可执行。如果执行成功,则返回消息队列的ID号(注意和队列KEY值作区分,这二者不同),否则返回-1。
8.msgsnd()函数和msgrcv()函数
msgsnd()函数的作用是将一个新的消息写入队列,msgrcv()函数的作用是从消息队列读取一个消息。函数格式是
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数msqid是消息队列的ID号;参数msgp是指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构,形态如下
struct msgbuf { long mtype; /* 消息类型,必须 > 0 */ char mtext[1]; /* 消息文本 */ };
参数msgsz是消息大小;参数msgtyp是消息类型(大于0则返回其类型为msgtyp的第一个消息,等于0则返回队列的最早的一个消息,小于0则返回其类型小于或等于mtype参数的绝对值的最小的一个消息),msgflag这个参数依然是是控制函数行为的标志(取值0,表示忽略,那么进程将被阻塞直到函数可以从队列中得到符合条件为止;取值IPC_NOWAIT,表示如果消息队列为空,则返回一个ENOMSG,并将控制权交回调用函数的进程)。
9.msgctl()函数
msgctl()函数的作用是对相应消息队列进程控制操作。函数格式是
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
参数msqid表示消息队列ID号;cmd表示对队列的控制操作,其可能值有IPC_STAT(读取消息队列的数据结构msqid_ds,并将其存储在buf指定的地址中)、IPC_SET(设置消息队列的数据结构msqid_ds中的ipc_perm元素的值,这个值取自buf参数)、IPC_RMID(从系统内核中移走消息队列);参数*buf用来表示队列的当前状态,可以设置为空。
10.一个消息队列通信的实例
编写一个receiver程序和一个sender程序。首先运行sender程序,建立一个消息队列,并向消息队列中发送一个消息。再运行receiver程序,从消息队列中接收一个消息,将其打印出来。
1 //文件名为sender.c 2 #include <sys/types.h> 3 #include <sys/msg.h> 4 #include <sys/ipc.h> 5 #include <stdio.h> 6 7 #define KEY 60 8 9 struct msgbuf 10 { 11 long mtype; //消息类型,必须大于0 12 char mtext[50]; //消息内容 13 }; 14 15 int main() 16 { 17 int msgqid; //消息队列ID号 18 struct msgbuf buf = { 1, "This is a message from sender\n"}; 19 msgqid=msgget(KEY,0777|IPC_CREAT); 20 msgsnd(msgqid, &buf, 50, 0); //发送消息到消息队列 21 return 0; 22 }
1 //文件名为receiver.c 2 #include <stdio.h> 3 #include <sys/types.h> 4 #include <sys/msg.h> 5 #include <sys/ipc.h> 6 7 #define KEY 60 8 9 struct msgbuf 10 { 11 long mtype; //消息类型,必须大于0 12 char mtext[50]; //消息内容 13 }; 14 15 int main() 16 { 17 int msgqid = 0; 18 struct msgbuf buf; 19 msgqid = msgget(KEY, 0777); 20 msgrcv(msgqid, &buf, 50, 0, IPC_NOWAIT); //接收一条最新消息,如果消息队列为空,不等待,直接返回错误标志 21 printf("%s", buf.mtext); 22 msgctl(msgqid, IPC_RMID, NULL); 23 24 return 0; 25 }
运行结果:
首先使用命令“ipcs -q”查看有无消息队列,开始时没有消息队列。运行sender程序后,再使用命令“ipcs -q”,可以看到有了一个消息队列(其中的key值为“0x3c”,十进制形式是60;“perms”项下为777,表示权限为任何用户可读、可写、可操作;“messages”项下为1,表示队列中有一条消息)。再运行receiver程序,读取出消息队列中的消息,将其打印出来。最后使用命令“ipcs -q”可以看到消息队列被销毁了。
11.消息队列机制用于进程同步
改写上述程序,要求实现以下功能:先运行receiver程序,使其处于阻塞状态。再运行sender程序,给receiver程序发送一条消息。receiver程序接收到消息后将其打印出来,然后结束。
1 //文件名为sender.c 2 #include <sys/types.h> 3 #include <sys/msg.h> 4 #include <sys/ipc.h> 5 #include <stdio.h> 6 7 #define KEY 60 8 9 struct msgbuf 10 { 11 long mtype; //消息类型,必须大于0 12 char mtext[50]; //消息内容 13 }; 14 15 int main() 16 { 17 int msgqid; //消息队列ID号 18 struct msgbuf buf = { 1, "This is a message from sender\n"}; 19 msgqid=msgget(KEY,0777); //打开名称为KEY的消息队列 20 msgsnd(msgqid, &buf, 50, 0); //发送消息到消息队列 21 return 0; 22 }
1 //文件名为receiver.c 2 #include <stdio.h> 3 #include <sys/types.h> 4 #include <sys/msg.h> 5 #include <sys/ipc.h> 6 7 #define KEY 60 8 9 struct msgbuf 10 { 11 long mtype; //消息类型,必须大于0 12 char mtext[50]; //消息内容 13 }; 14 15 int main() 16 { 17 int msgqid = 0; 18 struct msgbuf buf; 19 msgqid = msgget(KEY, 0777|IPC_CREAT); //创建一个消息队列,名称为KEY,该队列任何用户可读可写 20 msgrcv(msgqid, &buf, 50, 0, 0); //接收一条最新消息,如果消息队列为空,则阻塞,直到消息队列中有消息 21 printf("%s", buf.mtext); 22 msgctl(msgqid, IPC_RMID, NULL); 23 24 return 0; 25 }
运行结果
开始时,没有消息队列存在。首先运行receiver(&表示后台运行),使用命令“ps”可以看到后台有一个名称为receiver的进程在运行。然后运行sender,receiver接收到sender的消息后将其打印出来。再次使用“ps”命令,可以看到receiver进程已经销毁。该程序实现的主要原理是receiver的接收消息函数msgrcv使用了参数“0”,该参数的作用是如果消息队列中没有消息,则阻塞,等待消息的到来。
四、共享存储区
共享存储区是指在内存中开辟一个公共存储区,把要进行通信的进程的虚地址空间映射到共享存储区。发送进程向共享存储区中写数据,接收进程从共享存储区中读数据。