美文网首页
Linux进程间通信

Linux进程间通信

作者: 突击手平头哥 | 来源:发表于2020-03-15 14:17 被阅读0次

    进程间通信方式比较

    • 管道:速度慢,容量有限

    • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。

    • 信号量:不能传递复杂消息,只能用来同步

    • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了一块内存的。

    管道(pipe)

    • 仅用于父子进程间的通信

    • 每个管道包含两个文件描述符, 一个用于读取一个用于写入

    • 父子进程通常持有两个管道, 对于任一进程来说: 持有一个管道的写入描述符和另外一个管道的读取描述符

    函数原型

    #include <unistd.h>
     
    int pipe (int fd[2]);
    
    • f[0]用于读取, f[1]用于写入

    代码示例

    #include <sys/file.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <cstring>
    
    void server(int readfd, int writefd)
    {
        char buffer[128] = { 0 };
        int len = read(readfd, buffer, 128);
        printf("server recv: %s\n", buffer);
    
        strcpy(buffer, "this is server!");
        write(writefd, buffer, strlen(buffer));
    }
    
    void client(int readfd, int writefd)
    {
        char buffer[128] = { 0 };
        strcpy(buffer, "this is client!");
        write(writefd, buffer, strlen(buffer));
    
        int len = read(readfd, buffer, 128);
        printf("client recv: %s\n", buffer);
    }
    
    
    int main()
    {
        int pipe1[2] = { 0 };
        int pipe2[2] = { 0 };
    
        pipe(pipe1);
        pipe(pipe2);
    
        pid_t pid = fork();
    
        if(pid == 0)
        {
            close(pipe1[1]);
            close(pipe2[0]);
    
            //持有第一个管道用于读取的, 和第二个管道用于写入的
            client(pipe1[0], pipe2[1]);
        }
        else
        {
            close(pipe1[0]);
            close(pipe2[1]);
    
            server(pipe2[0], pipe1[1]);
            waitpid(pid, NULL, 0);
        }
        return 0;
    }
    

    资源消耗

    • 每个文件需要两个文件描述符

    • 如果没有数据的话可能会导致阻塞等待

    有名管道(FIFO)

    • 这是一个设备文件, 提供一个路径名与FIFO对应

    • 不需要亲缘关系, 只要可以访问该路径名即可

    • 与无名管道相比需要预先使用open打开

    接口

    #include <sys/types.h>
    #include <sys/stat.h>
    
    int mkfifo(const char *pathname, mode_t mode);
    
    • pathname指的是路径名

    • mode指明权限, 文件权限是: (mode & ~umask); 比如: 0666

    • 使用unlink删除

    示例(在两个文件中分别运行客户端和服务器)

    #include <sys/file.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <cstring>
    #include <sys/stat.h>
     #include <fcntl.h>
    
    #define FIFO_NAME "/tmp/test_fifo"
    
    void server()
    {
        mkfifo(FIFO_NAME, 0666);        //创建FIFO文件
    
        int fd = open(FIFO_NAME, O_WRONLY);
    
        printf("fd: %d\n", fd);
    
        write(fd, "this is server!", 15);
    
        close(fd);
    
        unlink(FIFO_NAME);
    }
    
    void client()
    {
        int fd = open(FIFO_NAME, O_RDONLY);
    
        printf("fd: %d\n", fd);
    
        char buffer[128] = { 0 };
        read(fd, buffer, 128);
        printf("client recv: %s\n", buffer);
        close(fd);
        
        return;
    }
    
    
    int main()
    {
        server();
        return 0;
    }
    
    • 如果采用: O_RDONLY 打开会阻塞, 直到有其他进程使用: O_WRONLY 打开; 反之亦然

    信号

    • 用户进程可以通过: signal/signalaction指定对信号的操作方式

    • 可以对进程本身发送信号, 也可以对其他进程发送信号

    常用接口

    int kill(pid_t pid, int sig);       //给指定进程发送信号
    
    int raise(int sig);                 //向进程自己发送信号
    
    unsigned int alarm(unsigned int seconds);       //设置定时器
    
    int pause(void);            //挂起直到收到信号
    
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);  //指定信号处理方式
    
    void abort(void);       //发送异常终止信号
    
    

    简单示例

    #include <sys/file.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <cstring>
    #include <signal.h>
    
    
    void handle(int sig)
    {
        printf("recv sig: %d\n", sig);
        raise(SIGKILL);
    }
    
    int main()
    {
        pid_t pid = fork();
    
        if(pid == 0)
        {
            printf("%s\n", "ss");
            signal(SIGTERM, handle);
    
            while(1) {};
            //无限循环
        }
        else
        {
            sleep(2);
            kill(pid, SIGTERM);
    
            waitpid(pid, NULL, 0);
        }
        return 0;
    }
    

    内存映射

    • 通过将某个设备映射到应用进程的内存空间, 通过直接的内存操作就可以完成对设备或文件的的读写

    接口说明

    • 1 头文件
    #include <sys/mman.h>
    
    • 2 创建内存映射
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    
    + addr: 将文件映射到内存空间指定地址, NULL即可
    + length: 映射到内存空间的内存块大小
    + prot: 访问权限, PROT_EXEC| PROT_READ | PROT_WRITE | PROT_NONE
    + flags: 程序对内存块的改变有什么影响
        + MAP_SHARED, 共享的, 内存块修改会保存到文件, 默认这个就可以
        + MAP_PRIVATE, 私有的, 修改只在局部范围有效
        + MAP_FIXED, 使用指定的映射起始地址
        + MAP_ANONYMOUS/MAP_ANON, 父子进程可以使用匿名映射, 文件描述符-1即可
    + fd: 文件描述符
    + offset: 从文件的哪里开始, 默认0即可
    
    + 返回映射的指针地址
    
    • 3 解除内存映射
    int munmap(void *addr, size_t length);
    
    + addr: mmap的返回值
    + length: 长度
    

    实例

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/mman.h>
     
    void server()
    {
        int fd;
        char buffer[128] = { 0 };
        strcpy(buffer, "this is server!");
     
        fd = open("/tmp/mmap_temp_file", O_RDWR|O_CREAT|O_TRUNC, 0644);
        ftruncate(fd, 64);  //64的大小
     
        // 使用fd创建内存映射区
        void* addr = mmap(NULL, 64, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd); // 映射完后文件就可以关闭了
     
        memcpy(addr, buffer, strlen(buffer)); // 往映射区写数据
    
        munmap(addr, 64); // 释放映射区
    }
    
    void client()
    {
        int fd;
     
        fd = open("/tmp/mmap_temp_file", O_RDONLY);
     
        // 使用fd创建内存映射区
        void* addr = mmap(NULL, 64, PROT_READ, MAP_SHARED, fd, 0);
        close(fd); 
    
        char *buffer = (char*)addr;
        printf("%s\n", buffer);
    
        munmap(addr, 64); // 释放映射区
    }
     
    
     
    int main() {
        server();
        return 0;
    }
    
    • MAP_SHARED也会更新文件

    • MAP_PRIVATE 只能此进程访问此数据

    消息队列

    • 生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除

    • 消息队列可以双向通信

    • 克服了管道只能承载无格式字节流的缺点;

    • 从队列读出后会被删除

    接口介绍

    创建和访问一个消息队列

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    int msgget(key_t key, int msgflag);
    
    • key, 消息队列的名称, 通过ftok产生

    • msgflag: IPC_CREAT和IPC_EXCL, 单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。

    • 返回一个消息队列的标识码

    ftok

    #include <sys/types.h>
    #include <sys/ipc.h>
    key_t ftok(const char *pathname, int proj_id);
    
    • pathname: 路径名

    • proj_id: 项目ID,非 0 整数(只有低 8 位有效)

    消息格式

    typedef struct _msg
    {
        long mtype;      // 消息类型
        char mtext[100]; // 消息正文
        //…… ……          // 消息的正文可以有多个成员
    }MSG;
    

    添加信息

    #include <sys/msg.h>
    int msgsnd(  int msqid, const void *msgp, size_t msgsz, int msgflg);
    
    • msqid: 即msgget的返回值

    • msgp 待发送消息结构体的地址。

    • msgsz 消息正文的字节数。

    • msgflag: 默认0即可

    获取信息

    #include <sys/msg.h>
    ssize_t msgrcv( int msqid, void *msgp,  size_t msgsz, long msgtyp, int msgflg );
    
    • msqid:消息队列的标识符,代表要从哪个消息列中获取消息。

    • msgp: 存放消息结构体的地址。

    • msgsz:消息正文的字节数。

    • msgtyp:消息的类型。可以有以下几种类型:

      • msgtyp = 0:返回队列中的第一个消息。
        +msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)。
        +msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
    • 在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进先出原则。

    消息队列的控制

    #include <sys/msg.h>
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    • 对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。

    • cmd:函数功能的控制。其取值如下:

      • IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关数据结构。
      • IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指向的结构中。相对于,把消息队列的属性备份到 buf 里。
      • IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中的对应值。相当于,消息队列原来的属性值清空,再由 buf 来替换。
    • buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。

    代码

    
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <string.h>
     
    typedef struct _msg
    {
        long mtype;
        char mtext[50];
    }MSG;
    
    void server()
    {
        struct _msg m = {100, "hihihih"};
    
        key_t key;
        int msgqid;
    
    
        key = ftok("./", 2015);
        msgqid = msgget(key, IPC_CREAT|0666);       //额外指定权限
    
    
        msgsnd(msgqid, &m, sizeof(m) - sizeof(long), 0);
        //正文大小
    }
    
    void client()
    {
        key_t key = ftok("./", 2015);
        int msgqid = msgqid = msgget(key, IPC_CREAT|0666); 
    
        struct _msg m;
        msgrcv(msgqid, &m, sizeof(m) - sizeof(long), 100, 0);
    
        msgctl(msgqid, IPC_RMID, NULL);
        printf("%s\n", m.mtext);
        return ;
    }
     
    
     
    int main() {
        client();
        return 0;
    }
    

    信号量

    • 信号量是描述资源可用性的计数器; 信号量可以通过创建一个值为1的信号量来专门锁定某个对象, 如果信号量的值大于零,则资源可用, 进程分配“资源的一个单元”,信号量减少一个

    • 传输的数据较少, 通常用于进程间同步; 可以和共享内存合作

    接口

    头文件

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h> 
    

    创建信号量集

    int semget(key_t key,int nsems,int flags)
    
    • key: 和消息队列一样使用ftok获得即可

    • 第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1

    • 第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作(如果再配合IPC_EXEC则已存在会报错)

    删除信号量

    int semctl(int semid, int semnum, int cmd, ...);
    
    • semid: 信号量标识符

    • semnum: 当前信号量集的哪一个信号量

    • cmd通常是下面两个值中的其中一个

      • SETVAL:用来把信号量初始化为一个已知的值, 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
      • IPC_RMID:删除信号量标识符,删除的话就不需要缺省参数
    • 第四个变量通常是: semun

    union semun
    { 
        int val;  //使用的值
        struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
        unsigned short *arry;  //GETALL,、SETALL 使用的数组
        struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
    };
    

    改变信号量的值

    int semop(int semid, struct sembuf *sops, size_t nops);
    
    • nsops: 进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作

    • sembuf的定义如下:

    struct sembuf{ 
        short sem_num;   //除非使用一组信号量,否则它为0 
        short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                        
                        //一个是-1,即P(等待)操作, 
                        //一个是+1,即V(发送信号)操作。 
        short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量, 
                      //并在进程没有释放该信号量而终止时,操作系统释放信号量 
    }; 
    

    代码实例

    
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    #include <string.h>
    
    void server()
    {
        key_t key;
        int semid;
        struct sembuf sb = { 0 };
        sb.sem_op = -1;
        sb.sem_flg = SEM_UNDO;
    
        key = ftok("./", 2015);
    
        semid = semget(key, 1, IPC_CREAT | 0666);   //创建信号量
    
        printf("%s\n", "Get Sem!");
        semop(semid, &sb, 1);
    
        semctl(semid, 0, IPC_RMID);     //删除
    
    }
    
    void client()
    {
        key_t key = ftok("./", 2015);
        int semid = semget(key, 1, IPC_CREAT|0666); 
    
        struct sembuf sb = { 0 };
        sb.sem_op = 1;
        sb.sem_flg = SEM_UNDO;
    
        semop(semid, &sb, 1);
        printf("%s\n", "Release Sem!");
    
        return ;
    }
     
    
     
    int main() {
        server();
        return 0;
    }
    

    共享内存

    • 使得多个进程可以访问同一块内存区域, 是最快可用的IPC形式, 往往和信号量结合使用, 达到进程间的同步与互斥

    接口

    创建共享内存

    #include<sys/ipc.h>
    #include<sys/shm.h>
    int shmget(key_t key,size_t size,int shmflg);
    
    • key: 类似于semget/msget, 通过ftok获得的标识符

    • size: 指定共享内存大小, 它的值一般为一页大小的整数倍(如果不到一页会对齐到一页)

    • shmflag: 和semflag/msgflag一样, 指定权限; 此外通过IPC_CREAT | IPC_EXCL创建新的

    将共享内存映射到虚拟地址空间

    #include<sys/types.h>
    #include<sys/shm.h>
    void * shmat (int shmid, const void * shmaddr, int shmflg);
    
    • shmid: 标识符

    • shmaddr: 映射地址, NULL即可

    • shm_flg: 一组标志位,通常为0

    操作共享内存

    int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
    
    • shm_id: 标识符

    • cmd: 三个值

      • IPC_STAT: 把shmid_ds结构中的数据设置为共享内存的当前关联值, 即用共享内存的当前关联值覆盖shmid_ds的值。
      • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
      • IPC_RMID:删除共享内存段(通常用这个就可以)

    分离操作

    int shmdt(const void *shmaddr);
    
    • 注意, 并没有删除标识符和数据结构

    代码实例(sem+shm)

    
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/sem.h>
    #include <string.h>
    
    void server()
    {
       key_t sem_key = ftok("./", 2016);        //信号量的key
       int sem_id = semget(sem_key, 1, IPC_CREAT | 0666);
    
        struct sembuf sb = { 0 };
        sb.sem_op = -1;
        sb.sem_flg = SEM_UNDO;
    
        semop(sem_id, &sb, 1);
        //如果获取了信号量, 表示另外以测已经写入了
    
        key_t shm_key = ftok("./", 2015);
        int shmid = shmget(shm_key, 64, IPC_CREAT | 0666);
        void *addr = shmat(shmid, NULL, 0);
    
        printf("Get Sem & Get [%s]\n", (char*)addr);
       
        semctl(sem_id, 0, IPC_RMID);     //删除信号量
    
        shmdt(addr);
        shmctl(shmid, IPC_RMID, NULL);      //删除共享内存
    
    }
    
    void client()
    {
        key_t key = ftok("./", 2015);
        int shmid = shmget(key, 64, IPC_CREAT | 0666);
    
        key_t sem_key = ftok("./", 2016);
        int semid = semget(sem_key, 1, IPC_CREAT|0666);
    
        struct sembuf sb = { 0 };
        sb.sem_op = 1;
        sb.sem_flg = SEM_UNDO;
    
        void *addr = shmat(shmid, NULL, 0);
    
        strcpy((char*)addr, "this is client!");
    
        semop(semid, &sb, 1);
        printf("%s\n", "Write Data & Release Sem!");
    
        shmdt(addr);
        return ;
    }
     
    
     
    int main() {
        server();
        return 0;
    }
    

    几种通信方式比较

    • 管道文件步占用磁盘空间, 管道读取完毕后会自动进入阻塞; 管道是半双工的, PIPE_SIZE限制为64k

    • 管道是没有边界的, 只能传递无格式字节流

    • 消息队列独立于进程外, 进程退出后数据依然存在; 要考虑上一次没有读完数据的问题

    • Linux下一个消息队列的最大字节数为 16k,系统中最多存在 16 个消息队列

    • 消息队列在使用完后需要手动删除

    • 信号/信号量: 不能用于传递复杂消息

    • 共享内存: 是最快的, 因为少了将数据从用户态复制到内核态的拷贝过程; 缺乏同步安全

    接口

    很多接口都是类似的, 做一下总结

    • 无名管道: pipe+write/read

    • 有名管道: mkfifo + open(注意阻塞) + read/write + unlink

    • 信号: kill+signal

    • 内存映射(文件映射到内存): open+mmap+munmap

    • 消息队列(通过key作为唯一标识): ftok+msgget+msgsnd+msgrcv+msgctl

    • 信号量: ftok+semget+semop+semctl

    • 共享内存: ftok+shmget+shmat+shmdt+shmctl

    相关文章

      网友评论

          本文标题:Linux进程间通信

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