Linux下进程间通信

Linux下进程间通信

Louis 1788 2021-04-05

本文介绍Linux下用到的各种进程间通信的方式。在没有读过《Linux高性能服务器编程》这本书之前,并不会使用Linux的内置IO函数进行进程间通信,更多的是依赖消息队列/ROS这些中间件来实现多进程的协调。

最近经过阅读,测试并实现了很多不同的进程间通信的API编程,打开了新世界大门。

pipe管道

创建管道

int pipe(int pipefd[2]); //成功:0;失败:-1,设置errno

函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。

#include <unistd.h>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <sys/wait.h>

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

int main() {
    pid_t pid;
    char buf[1024];
    int fd[2];
    if (pipe(fd) == -1)
        sys_err("pipe");
    char *p = "test for pipe\n";
    //fork返回两次,父线程返回子线程的pid,子线程则返回0
    pid = fork();
    if (pid < 0) {
        sys_err("fork err");
    } else if (pid == 0) {
        //子线程:关闭写端
        close(fd[1]);
        //read函数会阻塞直到数据可读
        int len = read(fd[0], buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);
        close(fd[0]);
    } else {
        //父线程:关闭读端
        close(fd[0]);
        write(fd[1], p, strlen(p));
        wait(nullptr);
        close(fd[1]);
    }
    return 0;

}

linux管道pipe详解_良月柒-CSDN博客_linux pipe

兄弟线程使用pipe通信

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
    pid_t pid;
    int fd[2], i;
    pipe(fd);
    for (i = 0; i < 2; i++) {
        if ((pid = fork()) == 0) {
            break;
        }
    }
    if (i == 0) { //兄
        close(fd[0]); //写,关闭读端
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
    } else if (i == 1) { //弟
        close(fd[1]); //读,关闭写端
        dup2(fd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
    } else {
        close(fd[0]);
        close(fd[1]);
        for (i = 0; i < 2; i++) //两个儿子wait两次
            wait(NULL);
    }
    return 0;
}

sendfile函数splice函数

sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。sendfile函数的定义如下:

#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd,off_t*offset,size_t count);

使用sendfile发送文件

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cassert>
#include<cstdio>
#include<unistd.h>
#include<cstdlib>
#include<cerrno>
#include<cstring>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/sendfile.h>

int main(int argc, char *argv[]) {
    if (argc <= 3) {
        printf("usage:%s ip_address port_number filename\n", basename(argv[0]));
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    const char *file_name = argv[3];
    int filefd = open(file_name, O_RDONLY);
    assert(filefd > 0);
    struct stat stat_buf{};
    fstat(filefd, &stat_buf);
    struct sockaddr_in address{};
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);
    int ret = bind(sock, (struct sockaddr *) &address, sizeof(address));
    assert(ret != -1);
    ret = listen(sock, 5);
    assert(ret != -1);
    struct sockaddr_in client{};
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr *) &client, &
            client_addrlength);
    if (connfd < 0) {
        printf("errno is:%d\n", errno);
    } else {
        sendfile(connfd, filefd, nullptr, stat_buf.st_size);
        close(confd);
    }
    close(sock);
    return 0;
}

linux内核系统调用--sendfile函数 - zfyouxi - 博客园 (cnblogs.com)

mmap函数

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int fd = 0;
    char *ptr = NULL;
    struct stat buf = {0};
    if (argc < 2) {
        printf("please enter a file!\n");
        return -1;
    }
    if ((fd = open(argv[1], O_RDWR)) < 0) {
        printf("open file error\n");
        return -1;
    }
    if (fstat(fd, &buf) < 0) {
        printf("get file state error:%d\n", errno);
        close(fd);
        return -1;
    }
    ptr = (char *) mmap(nullptr, buf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        printf("mmap failed\n");
        close(fd);
        return -1;
    }
    close(fd);
    printf("length of the file is : %ld\n", buf.st_size);
    printf("the %s content is : %s\n", argv[1], ptr);
    ptr[3] = 'a';
    printf("the %s new content is : %s\n", argv[1], ptr);
    //解除映射关系
    munmap(ptr, buf.st_size);
    return 0;
}

linux库函数mmap()原理_skybabybzh的专栏-CSDN博客_linux mmap

消息队列

消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。

// 创建和获取 ipc 内核对象
int msgget(key_t key, int flags);
// 将消息发送到消息队列
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);
// 查看、设置、删除 ipc 内核对象(用法和 shmctl 一样)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msg_send.cpp

#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char name[20];
    int age;
}Person;

typedef struct {
    long type;
    Person person;
}Msg;

int main(int argc, char *argv) {
    int id = msgget(0x8888, IPC_CREAT | 0664);
    
    Msg msg[10] = {
        {1, {"Luffy", 17}},
        {1, {"Zoro", 19}},
        {2, {"Nami", 18}},
        {2, {"Usopo", 17}},
        {1, {"Sanji", 19}},
        {3, {"Chopper", 15}},
        {4, {"Robin", 28}},
        {4, {"Franky", 34}},
        {5, {"Brook", 88}},
        {6, {"Sunny", 2}}
    };
    
    int i;
    for (i = 0; i < 10; ++i) {
        int res = msgsnd(id, &msg[i], sizeof(Person), 0);
    }
    
    return 0;
}

msg_recv.cpp

#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

typedef struct {
    char name[20];
    int age;
}Person;

typedef struct {
    long type;
    Person person;
}Msg;

void printMsg(Msg *msg) {
    printf("{ type = %ld, name = %s, age = %d }\n",
           msg->type, msg->person.name, msg->person.age);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("usage: %s <type>\n", argv[0]);
        return -1;
    }
    
    // 要获取的消息类型
    long type = atol(argv[1]);
    
    // 获取 ipc 内核对象 id
    int id = msgget(0x8888, 0);
   
    
    Msg msg;
    int res;
    
    while(1) {
        // 以非阻塞的方式接收类型为 type 的消息
        res = msgrcv(id, &msg, sizeof(Person), type, IPC_NOWAIT);
        if (res < 0) {
            // 如果消息接收完毕就退出,否则报错并退出
            if (errno == ENOMSG) {
                printf("No message!\n");
                break;
            }
        }
        // 打印消息内容
        printMsg(&msg);
    }
    return 0;
}

Linux系统编程—消息队列 - 简书 (jianshu.com)

尾声

以上,Linux实现进程间通信的API。在没有使用这些API之前,也可以使用其他中间件来实现不同进程间的通信,如ROS、RabbitMQ等。当然,使用这些原生的API可以达到更少的拷贝,提高程序的效率。

所以,其实同一个程序有多种不同的实现方案, 如果不考虑效率,他们都是可行的。使用更多的中间件会增加系统的复杂度,提高硬件的成本和维护成本。如无必要,勿增加实体。