zoukankan      html  css  js  c++  java
  • 操作系统之进程通信

    引子 进程通信的方式

      △信号通信

      △管道通信

      △消息队列

      △共享存储区

    一、信号通信

    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):如果信箱就绪(信箱中有消息),则从当前信箱指针指向的分区读取一条消息,否则返回状态信息(非阻塞式)或者等待信箱就绪(阻塞式)。

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

    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
    "};
    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
    "};
    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”,该参数的作用是如果消息队列中没有消息,则阻塞,等待消息的到来。

    四、共享存储区

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

  • 相关阅读:
    Java 面向对象(七)多态
    Java 面向对象(六)接口
    Java 面向对象(五)抽象
    JavaScript 之 String 对象
    JavaScript 之 基本包装类型
    JavaScript 之 Array 对象
    【LeetCode-数组】三数之和
    【LeetCode-数组】加一
    【LeetCode-数组】搜索插入位置
    【LeetCode-数组】删除排序数组中的重复项
  • 原文地址:https://www.cnblogs.com/UnfriendlyARM/p/10148520.html
Copyright © 2011-2022 走看看