操作系统之进程通信

Posted unfriendlyarm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统之进程通信相关的知识,希望对你有一定的参考价值。

引子 进程通信的方式

  △信号通信

  △管道通信

  △消息队列

  △共享存储区

一、信号通信

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
");  //等待结束后,打印提示信息
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
");  //等待结束后,打印提示信息
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
");  //等待结束后,打印提示信息
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
");  //等待结束后,打印提示信息
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
";
11       char *s2 = "The 2rd subprocess‘s data
";
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);  //向写句柄写入24个字节的数据
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):如果信箱就绪(信箱中有消息),则从当前信箱指针指向的分区读取一条消息,否则返回状态信息(非阻塞式)或者等待信箱就绪(阻塞式)。

  注:在信箱非空的情况下,每读取一次信箱,信箱中的消息就会少一条,直到信箱变为空状态。

6.消息通信的原理

  (1)如果一个进程要和另外一个进行通信,则这两个进程需要开辟一个共享存储区(信箱);

  (2)消息通信机制也可以用在一对多通信上,一个server和n个client通信时,那么server就和这n个client各建立一个共享存储区;

  (3)一个进程可以随时向信箱中存储消息,当然一个进程也可以随时从信箱中读取一条消息。

7.消息机制的同步作用

        采用消息队列通信机制,可以实现进程间的同步操作。在介绍同步功能之前,需要先介绍两个名词,阻塞式原语和非阻塞式原语。阻塞式原语是指某进程执行一个指令时,如果当前环境不满足执行条件,则该进程会在此停止,等待系统环境满足执行条件,然后继续向下执行。非阻塞式原语是指某进程执行一个指令时,如果当前环境不满足执行条件,则立即返回一个状态信息,并继续执行接下来的指令。

        (1)非阻塞式发送方+阻塞式接收方:两个进程开始运行后,接收方会进入等待状态,等待发送方给接收方发送一条消息,直到接收到相应的消息后,接收方进程才会继续向下执行。

        (2)非阻塞式发送方+非阻塞式接收方:发送方和接收方共享一个信箱,发送方随时可以向信箱中存入一条消息,接收方可以随时从信箱读取一条消息。当信箱满时,发送方进入阻塞状态;当信箱空时,接收方进入阻塞状态。

8.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。

9.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,并将控制权交回调用函数的进程)。

10.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用来表示队列的当前状态,可以设置为空。

11.一个消息队列通信的实例

        编写一个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
"};
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”可以看到消息队列被销毁了。

12.消息队列机制用于进程同步

  改写上述程序,要求实现以下功能:先运行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
"};
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”,该参数的作用是如果消息队列中没有消息,则阻塞,等待消息的到来。

四、共享存储区

        共享存储区是指在内存中开辟一个公共存储区,把要进行通信的进程的虚地址空间映射到共享存储区。发送进程向共享存储区中写数据,接收进程从共享存储区中读数据。

 

以上是关于操作系统之进程通信的主要内容,如果未能解决你的问题,请参考以下文章

linux进程间通信之System V共享内存详解及代码示例

操作系统之进程通信

进程间通信之共享内存

linux进程间通信之Posix 信号量用法详解代码举例

操作系统之 进程间通信的方式都有哪些

Linux系统编程之进程间通信