美文网首页
6. 进程间通信

6. 进程间通信

作者: 郑行_aover | 来源:发表于2019-03-28 18:21 被阅读0次

    参考链接:
    1. 进程间通信及使用场景
    2. 进程间通信机制IPC
    3. 看图理解进程间通信IPC==重点
    4. 进程间通信和同步


    1. 介绍

    在linux下有多中进程间的通信方法:

    • 半双工通道 PIPE
    • 命名管道 FIFO
    • 消息队列
    • 信号量
    • 共享内存
    • socket

    2. 无名管道(PIPE)

    应用

    • 在shell中使用,用于重定向
    • 用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。
      注意事项
    • 只能用于具有亲缘关系的进程之间的通信(父子进程或者兄弟进程)
    • 半双工通信模式,具有固定的读端和写端
    • 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read(),write()等函数.但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中.
    int fd[2];
    //定义
    int* write_fd = &fd[1];
    int* read_fd = &fd[0];
    //创建
    ret = pipe(fd);
    //读写
    write(*write_fd,string,size);
    read(*read_fd,buffer,sizeof(buffer));
    //关系
    close(*write_fd);
    close(*read_fd);
    

    管道的操作是阻塞性质的。

    管道读写注意事项

    • 只有在管道的读端存在时,向管道写入数据才有意义.否则,向管道写入数据的进程将收到内核传来的SIGPIPE信号(通常为Broken pipe错误).
    • 向管道写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据.如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞.
    • 父子进程在运行时,它们的先后次序并不能保证,因此,在为了保证父子进程已经关闭了相应的文件描述符,可在两个进程中调用sleep()函数,当然这种调用不是很好的解决方法,在后面学到进程之间的同步与互斥机制之后。

    3 .有名管道(FIFO)

    FIFO有时被称为命名管道,未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的祖先进程。但是,通过FIFO,不相关的进程之间也能交换数据。

    FIFO用途

    • (1) shell命令使用FIFO将数据从一条管道传送到另一条管道,无需创建中间临时文件。
      实例:考虑这样一个过程,他需要对一个输入文件进行两次处理,示意图如下
    • (2) 客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。
      创建FIFO:
    #include <sys/stat.h>
    int mkfifo(const char* path, mode_t mode);
    int mkfifoat(int fd, const char* path, mode_t mode);
    

    说明

    mkfifoat与mkfifo相似,像之前其他at系列函数一样,有3种情形:*

    • (1) 如果path参数指定了绝对路径名,则fd被忽略,此时mkfifoat和mkfifo一样。
    • (2) 如果path参数指定了相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关。
    • (3) 如果path参数指定了相对路径名,并且fd参数指定了AT_FDCWD,则路径名以当前目录开始,mkfifoat和mkfifo类似。

    当我们使用mkfifo或者mkfifoat函数创建FIFO时,要用open打开,确是,正常的I/O函数(如close、read、write、unlink)都需要FIFO。当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生如下影响:

    • (1) 没有指定O_NONBLOCK时,只读open要阻塞到某个其他进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其他进程为读而打开这个FIFO为止。

    • (2) 如果指定了P_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开这个FIFO,那么只写open将返回-1,并将errno设置为ENXIO。

        一个给定的FIFO有多个写进程是很常见的,这就意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作。和管道一样,常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量。
      

    实例

    • 考虑这样一个过程,他需要对一个输入文件进行两次处理,示意图如下:


      image.png

      我们可以使用FIFO和tee命令如下处理:

    mkfifo fifo1
    prog3 < fifo1 &
    prog1 < (输入文件) | tee fifo1 | prog2
    

    执行流程如下:

    image.png
    • 有一个服务器进程,它与很多客户进程相关,每个客户进程都可将请求写到一个该服务器进程创建的FIFO中。由于该FIFO有多个写进程,因此客户进程每次发送给服务器的数据长度要小于PIPE_BUF字节,这样就能避免客户进程之间的写交叉。


      image.png

    但是这种类型的FIFO设计有问题,服务器如何回应各个客户进程呢?

    1. 一种解决方法是,每个客户进程都在其请求中包含它的进程ID,然后服务器进程为每个客户进程创建一个FIFO,所使用的路径名是以客户进程的进程ID为基础的。
    2. 例如,服务进程可以用名字/tmp/serv1.XXXXX创建FIFO,其中XXXXX被替换成客户进程的进程ID,如下图所示: image.png

    4. 消息队列

    linux进程间通信-消息队列
    优点:消息队列与管道、FIFO相比,具有更大的灵活性

    • 提供格式字节流,减少开发人员的工作量
    • 消息具有类型,在实际应用中,可作为优先级使用
    • 消息队列可以在几个进程间复用,不用管这几个进程是否具备亲缘关系
    • 较为高效,不像共享内存一样还要自己处理竞争条件和临界代码区。

    缺点

    • 容量有限制,所能容纳的字节数
    • 每个消息队列所能容纳的最大消息数有限制
    4.1 有关消息队列的相关函数
    • int msgget(key_t key, int flag);
      msgget用于创建一个新队列或打开一个现有队列。

    • int msgsnd(int msqid, const void * ptr, size_t nbytes, int flag);
      msgsnd将新消息添加到队列尾端。每一个消息包含一个正的长整形类型的字段、一个非负的长度以及实际数据字节数,所有这些都在将消息添加到队列时,传送给msgsnd。

    • int msgctl(int msqid, int cmd, struct msqid_ds * buf);
      对队列执行cmd操作,例如:IPC_STAT(读取消息)IPC_SET(设置消息)IPC_RMID(删除消息)

    • ssize_t msgrcv(int msqid, void * ptr, size_t nbytes, long type,int flag);
      msgrcv用于从队列中取消息,我们不一定要已先进先出次序取消息,也可以按照消息的类型字段取消息。

    • key_t ftok(char* pathname ,char proj)
      返回与路径pathname相对应的一个键值。该函数不直接对消息队列操作。

    key = ftok(path_ptr,'a');
    ipc_id = ipc(MSGGET,(int)key,flags,0,NULL,0);
    
    

    4.2 消息队列基础理论

    消息队列就是一个消息的链表,每个消息队列都有一个队列头,用结构体struct msg_queue
    来描述。队列头中包含了该消息队列的大量信息,包括消息队列的键值、用户ID、组ID、消息队列中的消息数目等。

    struct kern_ipc_perm{
    //内核中记录消息队列的全局数据结构 msg_ids能够访问到该结构
        key_t key; //该键值则唯一对应一个消息队列
        key_t uid;
        key_t gid;
        key_t cuid;
        key_t cgid;
        mode_t mode;
        unsigned long seq;
    }
    

    读写操作
    消息读写操作非常简单,每个消息队列都有如下的数据结构:

    struct msgbuf{
        long mtype;
        char mtext[1];
    }
    
    • mtype 代表消息类型,读取消息的依据。
    • mtext 代表消息内容。

    获得或者设置消息队列的属性

    • msgctl(msgid,IPC_STAT,struct msqid_ds* Rbuf) 获得属性
    • msgctl(msgid,IPC_SET,struct msqid_ds* Wbuf) 设置属性

    完整示例代码

    
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <sys/msg.h>
    #include <unistd.h>
    
    void msg_stat(int ,struct msqid_ds);
    int main(int argc, char const *argv[])
    {
        int gflags,sflags,rflags;
        key_t key;
        int msgid;
        int reval;
        struct msgbuf
        {
            int mtype;
            char mtext[1];
        }msg_sbuf;
    
        struct msgbuf
        {
            int mtype;
            char mtext[10];
        }msg_rbuf;
    
        struct msqid_ds msg_ginfo,msg_sinfo;
        char* msgpath = "/unix/msgqueue";
        key = ftok(msgpath,'a');
        gflags = IPC_CREAT | IPC_EXCL;
    
        msgid = msgget(key,gflags | 00666);
        if(msgid == -1)
        {
            printf("msg queue creart error\n");
            return;
        }
    
        //创建消息队列后,输出消息队列的默认属性
        msg_stat(msgid,msg_ginfo);
        sflags = IPC_NOWAIT;
        msg_sbuf.mtype = 10;
        msg_sbuf.mtext[0] = 'a';
        //send msg
        reval = msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);
        if(reval == -1)
        {
            printf("messsge send error\n");
        }
        //发送消息后,输出消息队列属性
        msg_stat(msgid,msg_ginfo);
        rflags = IPC_NOWAIT | MSG_NOERROR;
        reval = msgrcv(msgid,&msg_rbuf,4,10,rflags);
        if(reval == -1){
            printf("read msg error\n");
        }else{
            printf("read from msg queue %d bytes\n",reval );
        }
        //从消息队列中读出消息后,输出消息队列的属性
        msg_stat(msgid,msg_ginfo);
        msg_sinfo.msg_perm.uid = 8;
        msg_sinfo.msg_perm.gid =8;
        msg_sinfo.msg_gbytes = 16388;
        // 此处验证超级用户可以更改消息队列的默认 msg_qbytes
        // 注意这里设置的值大于默认值
        reval = msgctl(msgid,IPC_SET,&msg_sinfo);
        if(reval == -1){
            printf("msg set info error\n");
            return;
        }
        msg_stat(msgid,msg_ginfo);
        // 验证设置消息队列属性
        reval = msgctl(msgid,IPC_RMID,NULL); //删除消息队列?
        if(reval == -1){
            printf("unlink msg queue error\n");
            return;
        }
    }
    
    void msg_stat(int msgid,struct msqid_ds msg_info)
    {
        int reval;
        sleep(1);
        reval = msgctl(msgid,IPC_STAT,&msg_info);
        if(reval == -1){
            printf("get msg info error\n");
            return;
        }
        printf("\n");
        printf("current number of bytes on queue is %d\n",msg_info.msg_cbytes );
        printf("number of message in queue is :%d\n", msgid_info.msg_qnum );
        printf("max number of bytes on queue is %d\n",msg_info.msg_qbytes );
        //每个消息队列的容量都有限制 MSGMNB,值的大小因系统而异。在创建新的消息队列时,msg_qbytes的默认值就是MSGMNB。
        printf("pid of last msgsnd is %d\n", msg_info.msg_ispid );
        printf("pid of last msgrcv is %d\n",msg_info.msg_lrpid );
        printf("last msgsnd time is  %s\n",ctime(&(msg_info.msg_stime)) );
        printf("last msgrcv time is  %s\n",ctime(&(msg_info.msg_rtime)) );
        printf("last change time is  %s\n",ctime(&(msg_info.msg_ctime)) );
    
        printf("msg uid is %d\n", msg_info.msg_perm.uid );
        printf("msg gid is %d\n", msg_info.msg_perm.gid);
    }
    
    

    5. 信号量

    [高质量嵌入式编程 274页]

    信号量主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时进程也可以修改该标志。用于访问控制及进程同步。

    • 二值信号灯:类似于互斥锁。值为0 或1.
      二值信号灯能够实现互斥锁的功能。信号灯强调共享资源,只要共享资源可用,其它进程同样可以修改信号灯的值。。互斥锁强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
    • 计算信号灯:值为任意非负值

    使用信号灯

    • 打开或创建信号灯
    • 信号灯操作,linux可以增加或减小信号灯的值,相当于对共享资源的释放和占有。
    • 获得信号灯的属性

    问题

    • 信号灯操作数目的限制,信号灯的最大数目,一个调用可操作的最大数,系统范围内的信号灯集数目都是有限制的。
    • 竞争问题:在创建第一个信号灯的进程同时也初始化信号灯。解决办法为:
      1. 创建第一个信号灯的进程必须调用semop,这样sem_otime才能变为非零值。
      2. 因为第一个进程可能不调用semop,或者semop操作需要很长时间。第二个进程可能无限期等待。

    实例
    【参看 高质量嵌入式linux c编程 278页】

    6. 共享内存

    • 最有用也是最快的进程间通信方式
    • 效率高
    • 管道和消息队列等通信方式,需要在内核和用户空间进行四次数据复制。共享内存复制两次。
    6.1 mmap
    • 头文件:#include <unistd.h> #include <sys/mman.h>
    • void *mmap(void* start, size_t length,int prot,int flags,int fd,off_t offsize);

    使用普通文件提供内存映射

    fd = open(name,flag,mode);
    if(fd<0)
    ptr = mmap(NULL,len,PROT_READ |PROT_WRITE,MAP_SHARED,fd,0);
    
    6.2 父子进程之间通过匿名映射实现共享
    typedef struct {
      char name[4];
      int age;
    }people;
    
    people* p_map;
    p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
    

    为了更安全的通信,通常共享内存需要和信号灯等同步机制共同使用。

    相关文章

      网友评论

          本文标题:6. 进程间通信

          本文链接:https://www.haomeiwen.com/subject/yxuqbqtx.html