Linux学习(二十):进程间通信

来源:互联网 发布:centos定时重启 编辑:程序博客网 时间:2024/06/05 02:36

1 进程间通信

        在Linux学习(十八):进程概念及创建中我们将fork函数中,我们说到子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等等,而子进程所独有的只有它的进程号、资源使用和计时器等。那是否可以直接使用全局变量来通信呢?我们来看一个例子
#include <stdio.h>#include <unistd.h>int x;int main(int argc, const char *argv[]){pid_t pid = fork();if(pid == -1){perror("fork error");return -1;}else if(pid == 0){//子进程 x = 5;printf("child  &x = %p, x = %d\n",&x,x);}else{//父进程 sleep(2);printf("parent &x = %p, x = %d\n",&x,x);}while(1);return 0;}
程序执行结果


        可以看到,虽然两个进程中x的地址是一样的,但是在父进程中,x的值并没有改变。这是为何?这是因为这个地址只是虚拟地址,并不是真正的物理地址,所以虽然就会出现这样的结果,也说明进程间通信并不能使用全局变量的方式。
        UNIX平台通信方式,早期进程间通信方式
        AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内。
        BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了该限制,形成了基于套接字(socket)的进程间通信机制。Linux继承了上述所有的通信方式。
        常用的进程间通信方式
        1、传统的进程间通信方式

        无名管道(pipe)、有名管道(fifo)和信号(signal)

        2、System V IPC对象

        共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)

        3、BSD

        套接字(socket)

2、管道通信

     管道是Linux中进程通信的一种方式,它把一个程序的输出直接连接到另一个程序的输入,Linux的管道主要包括两种:无名管道和有名管道。

2.1 无名管道

      无名管道具有如下特点:       

      1、只能用于具有亲缘关系的进程之间的通信

      2、半双工的通信模式,具有固定的读端和写端。

      3、管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO(不能使用标准IO)如read、write函数。无名管道并不是普通的文件,不属于任何文件系统,并且只存在于内存中。也就是在文件系统中不可见的。

     2.1.1 无名管道的创建

管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定
用于读管道,而fd[1]固定用于写管道。

创建函数pipe

        无名管道例程:从终端读取输入,写入管道中,然后再从管道中读出,并输出到终端中,写入quit时两个进程退出。
/*无名管道:用于具有亲缘关系的父子进程间通信*/#include <stdio.h>#include <stdlib.h>#include <unistd.h>#if 0int pipe(int pipefd[2]);功能:创建无名管道参数:pipefd[0]作为读端 pipefd[1]作为写端返回:成功返回0,失败-1注意:只能使用文件IO,read/write#endif#define N 32int main(int argc, const char *argv[]){char buf[N];int fd[2];//创建无名管道if(pipe(fd) == -1){perror("pipe error");  //fd[0]:读端  fd[1]:写端exit(1);}//创建子进程pid_t pid = fork();if(pid == -1){perror("fork error");exit(1);}else if(pid == 0){//子while(1){//从终端获取数据fgets(buf, N, stdin);//写到管道中write(fd[1], buf, N);//字符串比较if(strncmp(buf, "quit", 4) == 0){exit(0);}}}else{//父while(1){//从管道读取数据read(fd[0], buf, N);//字符串比较if(strncmp(buf, "quit", 4) == 0){exit(0);}//打印到终端printf("---> ");fputs(buf, stdout);}}return 0;}

        在创建管道要注意,要在fork()函数之前创建;若在fork()函数之后再创建,那每个进程都会创建管道,也就无法正常使用了。
        当管道中无数据时,读操作会阻塞。
        向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将会一直阻塞。
        只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号(通常Broken pipe错误)。

2.2 有名管道

有名管道(FIFO)是对无名管道的改进,具有如下特点:
     1、它可以使互不相关的两个进程实现彼此通信
     2、该管道可以通过路径名来实现,并且在文件中可见的。在建立管道后,两个进程就可以把它当做普通文件一样进程读写操作。当然这里要明白的,它的文件属性是管道文件p,在Linux学习(四):Linux文件系统及其shell命令中讲了Linux文件的七种类型,创建的管道属于管道文件。
   3、FIFO遵循先进先出原则,不支持lseek操作。
   4、对于读进程,若当前FIFO中无数据,会一直阻塞到有数据写入或FIFO写入端关闭
   5、对于写进程,只要FIFO有空间就可写入。若空间不足,写进程就会阻塞,直到有空间为止

2.2.1 创建有名管道mkfifo


    创建FIFO的出错信息

        对于错误码errno的使用,可以参考Linux学习(十六):文件IO。对于mkfifo()函数,EEXIST是比较常见的错误信息,表明FIFO已经创建,直接使用即可(这里可以理解为一个正常信息)。
有名管道例程:一个进程负责从终端读取数据,并写入进程,另一个进程从管道读取数据并向终端发送数据,若终端数据quit,两进程退出。
写管道:
/*有名管道:可以用于没有关系的进程间通信,先进先出规则*/#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <errno.h>#include <fcntl.h> #include <string.h> #if 0int mkfifo(const char *pathname, mode_t mode);功能:创建一个有名管道文件参数:pathname:指定创建文件的路径文件名  mode    :文件权限(mode & ~umask)返回:成功返回0,失败返回-1注意:管道文件不会重复创建,所以已存在是允许发生的错误#endif#define N 32int main(int argc, const char *argv[]){if(mkfifo("fifo", 0664) == -1){//已存在if(errno == EEXIST){puts("fifo exist");}else{perror("mkfifo error");exit(1);}}int fd_w = open("fifo", O_RDWR);if(fd_w == -1){perror("open error");exit(1);}//从终端读取数据,写到管道内,遇到"quit"退出char buf[N];while(1){fgets(buf, N, stdin);write(fd_w, buf, N);if(strncmp(buf, "quit", 4) == 0){break;}}close(fd_w);return 0;}

读管道:
/*有名管道:可以用于没有关系的进程间通信,先进先出规则*/#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <errno.h>#include <fcntl.h>#include <string.h> #if 0int mkfifo(const char *pathname, mode_t mode);#endif#define N 32int main(int argc, const char *argv[]){if(mkfifo("fifo", 0664) == -1){//已存在if(errno == EEXIST){puts("exist");}else{perror("mkfifo error");exit(1);}}int fd_r = open("fifo", O_RDWR);if(fd_r == -1){perror("open error");exit(1);}//从管道读取数据,打印到终端,遇到"quit"不输出并退出char buf[N];while(1){read(fd_r, buf, N);if(strncmp(buf, "quit", 4) == 0){break;}fputs(buf, stdout);}close(fd_r);return 0;}
任意执行一个进程后,会创建一个fifo文件,文件属性为p


两个进程的执行结果



3 信号通信

        信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

        信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
        如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程 。
        信号事件的产生有硬件来源和软件来源,常用的信号相关函数有kill()、alarm()、setitimer()、sigqueue()。
        进程可以通过3种方式来相应信号
        (1)忽略信号,对信号不做任何处理,SIGKILL和SIGSTOP两个信号不能忽略
        (2)捕捉信号,定义信号处理函数,当信号发生时,执行相应操作
        (3)执行默认操作。默认操作如下所示



3.1 信号发送函数kill

        kill函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill函数的一个
用户接口)。
        kill –l 命令查看系统支持的信号列表
       raise函数允许进程向自己发送信号


kill函数

kill例程:杀死一个进程

#include <stdio.h>#include <stdlib.h> #include <errno.h>#include <sys/types.h>#include <signal.h>#if 0int kill(pid_t pid, int sig);功能:给某一个进程或进程组发送信号sig参数:sig:要发送的信号  pid:pid > 0:给指定的进程发送信号   pid = 0:给同组下的所有进程发送信号(进程本身)   pid =-1:给所有进程发送信号(除了1号init进程)   pid <-1:给指定组下的所有进程发送信号,PGIG=|pid|返回:成功返回0,失败-1atoi功能:将数值型字符串转化成整数#endifint main(int argc, const char *argv[]) //argv[0] argv[1]{if(kill(atoi(argv[1]), SIGKILL) == -1){perror("kill error");exit(1);}puts("kill ok!");return 0;}
执行结果


我们将进程号2778的while进程(一个空的while(1))杀死


进程在收到SIGKILL信号后会在终端打印Killed。

3.2 信号设置signal()

signal()函数语法要点
所需头文件:#include<signal.h>
函数原型: typedef void(* sighandler_t)(int);
                 sighandler_t signal(int signum,sighandler_t handler)
参数:       参数1:指定信号
                 参数2:SIG_IGN 忽略该信号,其中SIGKILL和SIGSTOP不能忽略
                             SIG_DEFL 采用默认方式处理指定信号
                             自定义的信号处理函数,(sighandler_t 是一个重定义的函数指针,函数要求返回值为空,参数                                为int
返回值:   成功:信号处理函数地址
                出错:SIG_ERR
例程:

#include <stdio.h>#include <stdlib.h>#include <signal.h>#if 0typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);功能:捕捉一个指定的信号,并作相应的处理参数:signum :要捕捉的信号  handler:SIG_IGN 忽略     SIG_DFL 执行默认操作   fun    执行相应的函数代码返回:失败返回SIG_ERR注意:SIGKILL和SIGSTOP不能被捕捉#endifvoid fun(int sig){if(sig == SIGINT){puts("catch SIGINT");}if(sig == SIGTSTP){puts("catch SIGTSTP");}}int main(int argc, const char *argv[]){if(signal(SIGINT, fun) == SIG_ERR) //ctrl+C     SIGINT {perror("signal error");exit(1);}if(signal(SIGTSTP, fun) == SIG_ERR) //ctrl+\     SIGQUIT{perror("signal error");exit(1);}if(signal(SIGQUIT, SIG_DFL) == SIG_ERR) //ctrl+Z     SIGTSTP{perror("signal error");exit(1);}puts("signal");while(1);return 0;}
执行结果


我们用的ctrl+c和ctrl+z都不能正常结束程序了。而是执行相应的函数

4、IPC通信介绍

        以上三种属于传统进程间通信方式,下面介绍消息队列、共享内存和信号灯,这属于System V IPC方式。我们也称这三种通信方式为IPC对象通信。
       三种通信方式都需要一个标识符,共享内存标识符、消息队列标识符、信号灯标识符。 标识符只是IPC对象的内部名,如果多个进程需要在同一个IPC对象上进行回合通信,需要一个外部名。为此,使用了键值 (key值)。

    4.1 创建键值函数ftok()

头文件:       #include <sys/types.h>
               #include <sys/ipc.h>
函数原型:     key_t ftok(const char *pathname, int proj_id);
参数:          参数1:路径名 参数2 任意一个非0整形数据
返回值:      成功:返回键值,失败:-1

        4.2 IPC通信步骤

        对于消息队列、共享内存和信号灯通信来讲,第一步是打开或创建IPC通道。第二步就是去操作相应的IPC对象。部分函数是相近的(如打开或创建IPC通道函数,控制函数),方便记忆和使用。

        4.3 IPC相关shell命令

        1.ipcs命令用于查看系统中的IPC对象 ipcs -q,ipcs -m,ipcs -s 查看消息队列/共享内存/信号灯
        2.ipcrm命令用于删除系统中的IPC对象

       ipcrm -M shmkey  移除用shmkey创建的共享内存段
ipcrm -m shmid    移除用shmid标识的共享内存段
ipcrm -Q msgkey  移除用msqkey创建的消息队列
ipcrm -q msqid  移除用msqid标识的消息队列
ipcrm -S semkey  移除用semkey创建的信号
ipcrm -s semid  移除用semid标识的信号




5、消息队列

        消息队列是IPC对象的一种。消息队列由消息队列ID来唯一标识。消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列可以按照类型来发送/接收消息。我们之前学的管道通信和消息队列比较类似,但消息队列可以区别不同的消息(如消息类型1,消息类型2),管道中所有数据没有类别区分。

        5.1 创建或打开消息队列


        参数flag和open()函数中的flag类似。IPC_CREAT标志为创建一个先的IPC对象(若存在则不创建新的),IPC_EXCL也是创建一个新的IPC对象,若存在返回出错。后面再或上创建的IPC对象的权限,同样可用八进制表示。

        5.2 发送消息


        5.3 接收消息


       5.4 消息队列控制函数


例程:
发送消息文件,发送三个消息
/*消息队列:*/#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#if 0int msgget(key_t key, int msgflg);功能:创建并打开一个消息队列返回:成功返回消息队列的ID(非负数),失败-1int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);功能:向消息队列发送一条消息参数:msqid :消息队列的ID  msgp  :发送消息的地址  msgsz :消息中消息信息的大小  msgflg:0阻塞,IPC_NOWAIT非阻塞模式查看:ipcs -q删除:ipcrm -q ID号#endifstruct msgbuf{long mtype; //消息类型(>0)int a;float b;char c;};#define N sizeof(struct msgbuf) - sizeof(long)int main(int argc, const char *argv[]){//产生一个key值key_t key = ftok(".", 1);//创建并打开消息队列int msgid = msgget(key, IPC_CREAT|0664);if(msgid == -1){perror("msgget error");exit(1);}//发送消息 struct msgbuf msgbuf;msgbuf.mtype = 1;msgbuf.a = 10;msgbuf.b = 12.34;msgbuf.c = 'A';msgsnd(msgid, &msgbuf, N, 0);msgbuf.mtype = 2;msgbuf.a = 20;msgbuf.b = 22.34;msgbuf.c = 'B';msgsnd(msgid, &msgbuf, N, 0);msgbuf.mtype = 3;msgbuf.a = 30;msgbuf.b = 32.34;msgbuf.c = 'C';msgsnd(msgid, &msgbuf, N, 0);return 0;}

接收消息:接收消息类型为3的消息
/*消息队列:*/#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#if 0int msgget(key_t key, int msgflg);功能:创建并打开一个消息队列返回:成功返回消息队列的ID(非负数),失败-1int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);功能:向消息队列发送一条消息参数:msqid :消息队列的ID  msgp  :发送消息的地址  msgsz :消息中消息信息的大小  msgflg:0阻塞,IPC_NOWAIT非阻塞模式ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);功能:接收消息参数:msgtyp:=0 接收第一条消息  >0 接收类型为msgtyp的第一条消息  <0 接收小于等于|msgtyp|中最小的第一条数据  msgflg:0阻塞,IPC_NOWAIT非阻塞模式int msgctl(int msqid, int cmd, struct msqid_ds *buf);功能:删除消息队列、获取属性信息、设置属性参数:cmd:IPC_RMID  IPC_STAT   IPC_SET返回:成功返回0  失败返回-1查看:ipcs -q删除:ipcrm -q ID号#endifstruct msgbuf{long mtype; //消息类型(>0)int a;float b;char c;};#define N sizeof(struct msgbuf) - sizeof(long)int main(int argc, const char *argv[]){//产生一个key值key_t key = ftok(".", 1);//创建并打开消息队列int msgid = msgget(key, IPC_CREAT|0664);if(msgid == -1){perror("msgget error");exit(1);}//接收消息 struct msgbuf msgbuf;msgrcv(msgid, &msgbuf, N, 3, 0);printf("a = %d, b = %.2f, c = %c\n",msgbuf.a, msgbuf.b, msgbuf.c);//删除消息队列system("ipcs -q");msgctl(msgid, IPC_RMID, NULL);system("ipcs -q");return 0;}

执行结果:

6、共享内存

        共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等 。
共享内存我们可以类比成普通程序中的全局变量,全局变量对于所有函数都是可见的,而共享内存对于多个进程都是可操作的,当然需要有一定的步骤,如下:
1、创建/打开共享内存

2、映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问

3、撤销共享内存映射

4、删除共享内存对象

       6.1 创建共享内存



       6.2 映射共享内存



        6.3 撤销共享内存


        6.3 共享内存控制


7、信号灯

       信号灯主要用于进程间的同步,对于信号量,我们可以用停车场外面指示牌简单类比一下,停车场是一个公用的资源,但能停的车是有限的,比如一个停车场最多可以停10辆车;停车场的指示牌最初显示是10,这是车辆可以进入停车场,每次进一辆指示牌就减一,当指示牌显示为0的时候,外面的车就不能再进入停车场,也就是不能占用这项资源。但此时如果有停车场内的车出来了,指示牌就可以加1,停车场这个资源又可以使用了。
     信号灯使用步骤
     1、创建信号量
     2、初始化信号量
     3、进行信号量PV操作,P操作也就是占用一个资源,信号量减1。V操作,释放一个资源,信号量加1。
     4、删除信号量

    7.1 创建信号量

     所需头文件:#include <sys/types.h>
                #include <sys/ipc.h>
                #include <sys/sem.h>
     函数原型:int semget(key_t key, int nsems, int semflg);
     功能:创建并打开信号灯
     参数:key:键值;nsems:指定信号灯中信号量的个数。semflg:权限
     返回:成功返回ID,失败-1

     7.2 信号量控制

     int semctl(int semid, int semnum, int cmd, union semun);
     参数:semid 信号量ID
           semnum:信号灯中信号量的编码(从0开始)
           cmd   :IPC_RMID(删除)  IPC_STAT(获取信号量状态)  IPC_SET
             SETVAL(设置信号量值)    GETVAL(获取信号量值)
           semun :使用SETVAL使用第四个参数
  

    7.3 操作信号量

     int semop(int semid, struct sembuf *sops, unsigned nsops);
     功能:执行PV操作
     参数:nsops:指定同时操作信号量的个数
           sops :
                 unsigned short sem_num;  //信号量的编码
                 short          sem_op;   //正数执行V操作,负数执行P操作
                 short          sem_flg;  //0阻塞,IPC_NOWAIT非阻塞
     返回:成功返回0,失败-1
例程:获取终端输入的数据,放入共享内存中,再打印出来。通过信号灯进行同步。
读取终端数据程序
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include <sys/sem.h>#include <string.h> #define N 32union semun{int val;};int main(int argc, const char *argv[]){//key值key_t key = ftok(".", 1);/***************** 共享内存 ********************///创建共享内存int shmid = shmget(key, N, IPC_CREAT|0664);if(shmid == -1){perror("shmget error");exit(1);}//映射char *p = NULL;p = (char *)shmat(shmid, NULL, 0);/****************** 信号灯 *********************///创建信号灯int semid = semget(key, 1, IPC_CREAT|0664);if(semid == -1){perror("semget error");exit(1);}//设置信号量值union semun semun;semun.val = 0;semctl(semid, 0, SETVAL, semun);//V操作(增加)struct sembuf sembuf;sembuf.sem_num = 0; //信号量编码sembuf.sem_op  = 1; //增加信号量值sembuf.sem_flg = 0; //阻塞模式/*******************  通信 **********************/while(1){//从终端获取数据写到共享内存fgets(p, N, stdin);//执行V操作(增加)semop(semid, &sembuf, 1);if(strncmp(p ,"quit",4) == 0){break;}}//解除映射shmdt(p);return 0;}
从共享内存获取数据,并打印到终端中去
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include <sys/sem.h>#include <string.h> #define N 32int main(int argc, const char *argv[]){//key值key_t key = ftok(".", 1);/***************** 共享内存 ********************///创建共享内存int shmid = shmget(key, N, IPC_CREAT|0664);if(shmid == -1){perror("shmget error");exit(1);}//映射char *p = NULL;p = (char *)shmat(shmid, NULL, 0);/****************** 信号灯 *********************///创建信号灯int semid = semget(key, 1, IPC_CREAT|0664);if(semid == -1){perror("semget error");exit(1);}//P操作(减少)struct sembuf sembuf;sembuf.sem_num = 0; //信号量编码sembuf.sem_op  = -1; //增加信号量值sembuf.sem_flg = 0; //阻塞模式/*******************  通信 **********************/while(1){//执行P操作(减少)semop(semid, &sembuf, 1);if(strncmp(p, "quit", 4) == 0){break;}//从共享内存读取数据打印到终端fputs(p, stdout);}/*******************  删除 **********************///解除映射shmdt(p);//删除共享内存shmctl(shmid, IPC_RMID, NULL);system("ipcs -m");//删除信号灯semctl(semid, 0, IPC_RMID);system("ipcs -s");return 0;}









原创粉丝点击