进程间通信

对进程间通信进行一个简单介绍

进程间通信

  • 以传输数据为目的
    • 管道
    • 共享内存
    • 消息队列
    • 套接字
  • 以同步状态为目的
    • 消息队列
    • 信号量

管道

匿名管道

匿名管道是Linux中最简单的一种进程间(半双工)通信的方式。

#include <unistd.h>
int pipe(int pipefd[2]);

pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe.
例子:

void pipe_demo()
{
    int result = -1;
    int fd[2];
    pid_t pid;

    result = pipe(fd);
    if (result == -1) {
        printf("create pipe failed\n");
        return;
    }

    pid = fork();
    if (pid < 0) {
        printf("create process failed\n");
        return;
    } else if (pid > 0) {
        printf("main process: %d\n", getpid());
        printf("%d start write\n", getpid());
        char write_str[] = "hello from main";
        write(fd[1], write_str, sizeof(write_str));

    } else {
        printf("child process: %d\n", getpid());
        printf("%d start read\n", getpid());
        char read_str[32] = "";
        read(fd[0], read_str, sizeof(read_str) - 1);
        printf("%s\n", read_str);
    }
    wait(NULL);
}

需要注意的是,fd[0]和fd[1]本质上是一个通道的两头,如果在进程内先写fd[1]后立马读fd[0],那么你大概率读到的是自己写的东西,你需要等子进程往fd[1]中写了东西再读(但一般不会这样使用,每对fd都是半双工单向通信的,你想要全双工,那么只能新建一对fd了)。

命名管道FIFO

与匿名管道不同,命名管道:

  1. 在文件系统中命名管道是以设备特殊文件的形式存在的
  2. 不同的进程可以通过命名管道共享数据

命名管道是需要程序自己创建的:

  • 通过mkfifo命令直接创建
    • mkfifo /tmp/myfifo
  • 通过mkfifo的接口创建
    • int mkfifo(const char *pathname, mode_t mode);

对FIFO来说,由于是一个具名的设备文件,那么对它的IO操作就是通过open获取的fd来进行的。
例子:

void fifo_demo()
{
    int result = -1;
    pid_t pid;

    result = mkfifo(myfifo, S_IFIFO | S_IRWXU);
    if (result == -1) {
        if (EEXIST != errno) {
            printf("create fifo file failed: %s\n", strerror(errno));
            return;
        }
    }

    pid = fork();
    if (pid < 0) {
        printf("create process failed\n");
        return;
    } else if (pid > 0) {
        int fd = open(myfifo, O_RDWR);
        if (fd < 0) {
            printf("open fifo file error\n");
            wait(NULL);
            return;
        }
        printf("main process: %d\n", getpid());
        printf("%d start write\n", getpid());
        char write_str[] = "hello from main";
        write(fd, write_str, sizeof(write_str));

    } else {
        int fd = open(myfifo, O_RDWR);
        if (fd < 0) {
            printf("open fifo file error\n");
            wait(NULL);
            return;
        }
        printf("child process: %d\n", getpid());
        printf("%d start read\n", getpid());
        char read_str[32] = "";
        read(fd, read_str, sizeof(read_str) - 1);
        printf("%s\n", read_str);
    }
    wait(NULL);
}

需要注意的是,调用open()打开命名管道的进程可能会被阻塞。但如果同时用读写方式( O_RDWR)打开,则一定不会导致阻塞;如果以只读方式( O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;同样以写方式( O_WRONLY)打开也会阻塞直到有读方式打开管道。

消息队列

消息队列是内核地址空间中的内部链表,通过Linux内核在各个进程之间传递内容。消息顺序地发送到消息队列中,每个消息队列可以用IPC标识符唯一地进行标识。内核中的消息队列是通过IPC的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一个独立的链表。
接口存在system V IPC和POSIX IPC两种接口。

system V IPC

POSIX IPC

内核中的消息队列关系

信号量

信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。他们通常被用作一个锁机制,在某个进程正在对特定资源进行操作时,信号量可以防止另一个进程去访问它。
存在system V IPC和POSIX IPC两种接口。

system V IPC

POSIX IPC

内核中的信号量关系

共享内存

共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程之间对内存段进行映射的方式实现内存共享的。这是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则需要将数据通过中间机制进行转换;与此相反,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅是地址不同而已,因此不需要进行复制,可以直接使用此段空间。
存在system V IPC和POSIX IPC两种接口。

system V IPC

POSIX IPC

内核中的共享内存关系

信号

信号(signal)机制是Unix/Linux系统中最早的进程间的通信机制。它用于在一个或多个进程之间传递异步信号。
修改信号处理:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

int sigaction(int signum,
                     const struct sigaction *_Nullable restrict act,
                     struct sigaction *_Nullable restrict oldact);

发送信号:

#include <signal.h>
int kill(pid_t pid, int sig);

int raise(int sig);

kill向进程号为pid的进程发送信号,信号值为sig。当pid为0时,向当前系统内的所有进程发送信号。
raise在当前进程中自举一个信号sig,即向当前进程发送信号。

套接字

网络套接字

一种网络IPC

本地套接字/UNIX域套接字

本地套接字有两种类型:字节流套芥子和数据报套接字,字节流套接字类似于TCP,数据报套接字类似于UDP。
套接字地址类型:

#include <sys/un.h>
struct sockaddr_un {
      sa_family_t     sun_family;     /* Address family */
      char            sun_path[];     /* Socket pathname */
};

本地套接字的创建:

void domain_sock_demo()
{
    int error;
    int sock_unix;
    struct sockaddr_un addr_unix;
    int len_unix;

    sock_unix = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_unix < 0) {
        printf("create unix socket error: %s\n", strerror(errno));
        return;
    }
    // 删除残留路径
    unlink(mysock);

    memset(&addr_unix, 0, sizeof(addr_unix));
    addr_unix.sun_family = AF_LOCAL;
    strncpy(addr_unix.sun_path, mysock, strlen(mysock));
    len_unix = SUN_LEN(&addr_unix); 
    addr_unix.sun_path[0] = 0; // 使用格式化抽象本地地址

    error = bind(sock_unix, (struct sockaddr *)&addr_unix,
                 sizeof(struct sockaddr_un));
    if (error < 0) {
        printf("bind unix sock error: %s\n", strerror(errno));
        return;
    }

    while (1) { ; }

    close(sock_unix);
    unlink(mysock);

    return;
}

其中,在填充本地套接字名称时,使用了格式化抽象本地地址的技术,使得路径名的第一个字节为一个空字节。在路径名中空字节之后的字节才会成为抽象名字的一部分:

unix  2      [ ]         STREAM                   84015    12681/./c_snippet    @tmp/mysock@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

如果不使用格式化抽象本地地址,那么就要保证指定的本地套接字路径上的所有文件夹是存在的,否则创建socket会失败:No such file or directory
在计算实际sockaddr_un大小时,可以使用SUN_LEN宏来计算:

/* Evaluate to actual length of the `sockaddr_un' structure.  */
# define SUN_LEN(ptr) (offsetof (struct sockaddr_un, sun_path)		      \
		      + strlen ((ptr)->sun_path))

两个进程通过本地套接字通信:

void domain_sock_demo()
{
    int pid;
    pid = fork();
    if (pid < 0) {
        printf("fork error: %s", strerror(errno));
        return;
    } else if (pid > 0) {
        printf("main process: %d\n", getpid());
        domain_sock_server_demo();
    } else if (pid == 0) {
        printf("child process: %d\n", getpid());
        domain_sock_client_demo();
    }
    wait(NULL);
}

void domain_sock_server_demo()
{
    int error;
    int sock_unix;
    struct sockaddr_un addr_unix;
    int len_unix;

    sock_unix = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_unix < 0) {
        printf("create unix socket error: %s\n", strerror(errno));
        return;
    }
    // 删除残留路径
    unlink(mysock_serv);

    memset(&addr_unix, 0, sizeof(addr_unix));
    addr_unix.sun_family = AF_LOCAL;
    strncpy(addr_unix.sun_path, mysock_serv, strlen(mysock_serv));
    len_unix = SUN_LEN(&addr_unix);
    // addr_unix.sun_path[0] = 0; // 使用格式化抽象本地地址

    error = bind(sock_unix, (struct sockaddr *)&addr_unix, len_unix);
    if (error < 0) {
        printf("bind unix sock error: %s\n", strerror(errno));
        return;
    }

    if (listen(sock_unix, 128) < 0) {
        printf("listen on unix socket error: %s\n", strerror(errno));
        close(sock_unix);
        unlink(mysock_serv);
        return;
    }

    struct sockaddr_un addr_peer_unix;
    socklen_t len_peer = sizeof(addr_peer_unix);
    int connfd =
        accept(sock_unix, (struct sockaddr *)&addr_peer_unix, &len_peer);
    if (connfd < 0) {
        printf("accept on unix sock error: %s\n", strerror(errno));
        close(sock_unix);
        unlink(mysock_serv);
        return;
    }

    len_peer -= offsetof(struct sockaddr_un, sun_family);
    char *name = (char *)malloc(len_peer + 1);
    strncpy(name, addr_peer_unix.sun_path, len_peer);
    name[len_peer] = 0;
    struct stat stat_buf;
    if (stat(name, &stat_buf) < 0) {
        printf("%s not stat: %s\n", name, strerror(errno));
        free(name);
        close(sock_unix);
        unlink(mysock_serv);
        return;
    }

    // read
    char buff[128] = "";
    int readret = read(connfd, buff, sizeof(buff));
    if (readret < 0) {
        printf("read error: %s\n", strerror(errno));
    } else if (readret == 0) {
        printf("peer closed\n");
    } else {
        printf("peer send: %s\n", buff);
    }

    sleep(2);
    close(sock_unix);
    unlink(mysock_serv);

    return;
}

void domain_sock_client_demo()
{
    int ret;
    int sock_unix;
    struct sockaddr_un addr_unix, addr_unix_serv;
    int len_unix, len_unix_serv;

    sock_unix = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_unix < 0) {
        printf("create unix socket error: %s\n", strerror(errno));
        return;
    }
    // 删除残留路径
    unlink(mysock_peer);

    memset(&addr_unix, 0, sizeof(addr_unix));
    addr_unix.sun_family = AF_LOCAL;
    strncpy(addr_unix.sun_path, mysock_peer, strlen(mysock_peer));
    len_unix = SUN_LEN(&addr_unix);
    // addr_unix.sun_path[0] = 0; // 使用格式化抽象本地地址

    ret = bind(sock_unix, (struct sockaddr *)&addr_unix, len_unix);
    if (ret < 0) {
        printf("bind unix sock error: %s\n", strerror(errno));
        close(sock_unix);
        return;
    }

    addr_unix_serv.sun_family = AF_LOCAL;
    strncpy(addr_unix_serv.sun_path, mysock_serv, strlen(mysock_serv));
    len_unix_serv = SUN_LEN(&addr_unix_serv);
    if (connect(sock_unix, (struct sockaddr *)&addr_unix_serv, len_unix_serv)
        < 0) {
        printf("connect to sock server failed: %s\n", strerror(errno));
        close(sock_unix);
        return;
    }

    int write_len =
        write(sock_unix, "hello from peer", sizeof("hello from peer"));
    if (write_len < 0) {
        printf("write to sock error: %s\n", strerror(errno));
    } else if (write_len == 0) {
        printf("serv closed\n");
    } else {
        printf("write count: %d\n", write_len);
    }

    sleep(2);
    close(sock_unix);
    unlink(mysock_peer);

    return;
}

匿名套接字

#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
  • domain:表示协议族,只能为AF_LOCAL或者AF_UNIX;
  • type:表示类型,只能为0;
  • protocol:表示协议,可以是SOCK_STREAM或者SOCK_DGRAM;用SOCK_STREAM建立的套接字对是管道流,与一般的管道相区别的是,套接字对建立的通道是双向的,即每一端都可以进行读写。
  • sv:用于保存建立的套接字对

socketpair函数建立了一对匿名的已经连接的套接字,其特性由协议domain、类型type、协议protocol决定,建立的两个套接字描述符会放在sv[0]和sv[1]中。

一般使用流程:使用socketpair建立套接字描述符后,在一个进程中关闭其中的一个,在另一个进程中关闭另一个。
例子:

void domain_socketpair_demo()
{
    int sockfd[2];
    int ret;
    pid_t pid;
    ret = socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
    if (ret != 0) {
        printf("make socket pair failed: %s\n", strerror(errno));
        return;
    }
    pid = fork();
    if (pid < 0) {
        printf("fork error: %s", strerror(errno));
        return;
    } else if (pid > 0) {
        printf("main process: %d\n", getpid());
        close(sockfd[1]);
        char str[] = "hello from main";
        write(sockfd[0], str, sizeof(str));
        sleep(1);
        close(sockfd[0]);
    } else if (pid == 0) {
        printf("child process: %d\n", getpid());
        close(sockfd[0]);
        char str[128] = "";
        int read_chars = read(sockfd[1], str, sizeof(str));
        if (read_chars < 0) {
            printf("read error: %s\n", strerror(errno));
        } else if (read_chars == 0) {
            printf("peer socket closed\n");
        } else {
            printf("read %d from peer: %s\n", read_chars, str);
        }
        close(sockfd[1]);
        return;
    }
    wait(NULL);
    return;
}

传递文件描述符

Linux支持从一个进程中将一个已经打开的文件描述符传递给其他的进程,基本流程:

  • 创建一个字节流或者数据报的UNIX域套接字
    • 如果目标是fork一个子进程,让子进程打开描述符并将它返回给父进程,那么父进程可以用socketpair创建一个流管道,用它来传递描述字
    • 如果进程之间没有亲缘关系,那么服务器必须创建一个UNIX域字节流套接字,绑定一个路径名,让客户连接到这个套接字。然后客户端可以向服务器发送一个请求打开某个描述字,服务器将描述符通过UNIX域套接字传回。在客户端和服务器之间也可以用UNIX 数据报套接字,但不建议这样使用
  • 进程可以用任何返回描述符的UNIX函数打开,可以在进程间传递任何类型的描述符。
  • 发送进程建立一个msghdr结构,其中包含要传递的描述符。发送进程调用sendmsg通过第一步得到的UNIX域套接字发出描述符。这个时候描述符是在飞行中的,即在发送进程调用sendmsg后,在接收进程调用recvmsg之前将描述符关闭,它仍会为接收进程保持打开状态,描述符的发送导致它的访问统计数加1
  • 接收进程调用recvmsg在UNIX域套接字上接收套接字。通常接收进程收到的描述符的编号和发送进程中的描述符的编号不同。但这没问题,传递描述符不是传递描述符的编号,而是在接收进程中建立一个新的描述符,指向内核的文件表中与发送进程发送的描述符相同的项。
    例子:
/**
 * @brief send a fd
 * @param  sock_unix    domain socket
 * @param  data msg data
 * @param  bytes msg data bytes
 * @param  send_fd  fd to send by domain socket
 */
void send_fd(int sock_unix, void *data, int bytes, int send_fd)
{
    struct msghdr msghdr_send;
    struct iovec iov[1];
    char control[CMSG_SPACE(sizeof(int))];

    struct cmsghdr *pcmsghdr = NULL;
    msghdr_send.msg_control = control;
    msghdr_send.msg_controllen = sizeof(control);

    pcmsghdr = CMSG_FIRSTHDR(&msghdr_send);
    pcmsghdr->cmsg_len = CMSG_LEN(sizeof(int));
    pcmsghdr->cmsg_level = SOL_SOCKET;
    pcmsghdr->cmsg_type = SCM_RIGHTS;
    *((int *)CMSG_DATA(pcmsghdr)) = send_fd;

    msghdr_send.msg_name = NULL;
    msghdr_send.msg_namelen = 0;

    iov[0].iov_base = data;
    iov[0].iov_len = bytes;
    msghdr_send.msg_iov = iov;
    msghdr_send.msg_iovlen = 1;

    if (sendmsg(sock_unix, &msghdr_send, 0) < 0) {
        printf("sendmsg failed: %s\n", strerror(errno));
    }
}

/**
 * @brief recv a fd
 * @param  sock_unix    connected domain socket
 * @param  data msg data
 * @param  bytes    msg bytes
 * @param  recv_fd  fd to recv by domain socket
 * @return int recv msg len
 */
int recv_fd(int sock_unix, void *data, int bytes, int *recv_fd)
{
    struct msghdr msghdr_recv;
    struct iovec iov[1];
    ssize_t n;

    char control[CMSG_SPACE(sizeof(int))];

    struct cmsghdr *pcmsghdr;
    msghdr_recv.msg_control = control;
    msghdr_recv.msg_controllen = sizeof(control);

    msghdr_recv.msg_name = NULL;
    msghdr_recv.msg_namelen = 0;

    iov[0].iov_base = data;
    iov[0].iov_len = bytes;
    msghdr_recv.msg_iov = iov;
    msghdr_recv.msg_iovlen = 1;
    if ((n = recvmsg(sock_unix, &msghdr_recv, 0)) <= 0) {
        printf("recvmsg nothing: %s\n", strerror(errno));
        return n;
    }

    pcmsghdr = CMSG_FIRSTHDR(&msghdr_recv);
    if (pcmsghdr != NULL && pcmsghdr->cmsg_len == CMSG_LEN(sizeof(int))) {
        if (pcmsghdr->cmsg_level != SOL_SOCKET) {
            printf("control level is not socket!\n");
        }
        if (pcmsghdr->cmsg_type != SCM_RIGHTS) {
            printf("control type is not SCM_RIGHTS!\n");
        }
        *recv_fd = *(int *)CMSG_DATA(pcmsghdr);
    } else {
        *recv_fd = -1;
    }
    return n;
}

server端:

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int sock_unix = create_domain_sock(mysock_serv);
    if (sock_unix < 0) {
        printf("create domain sock failed!\n");
        return -1;
    }
    printf("create and bind unix socket down\n");

    if (listen(sock_unix, 128) < 0) {
        printf("listen on unix socket error: %s\n", strerror(errno));
        close(sock_unix);
        unlink(mysock_serv);
        return -1;
    }

    int connfd = accept_domain_sock(sock_unix);
    if (connfd < 0) {
        printf("accept sock failed!\n");
        close(sock_unix);
        unlink(mysock_serv);
        return -1;
    }
    printf("accept an unix socket down\n");

    char c;
    int fd;
    recv_fd(connfd, &c, 1, &fd);
    printf("recvmsg from unix socket down\n");
    if (fd > 0) {
        write(fd, "hello from server", sizeof("hello from server"));
        close(fd);
    }

    sleep(1);

    close(sock_unix);
    unlink(mysock_serv);

    return 0;
}

client端:

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int fd = open(myfile, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
    if (fd < 0) {
        printf("open file failed: %s\n", strerror(errno));
        return -1;
    }

    int sock_unix = create_domain_sock(mysock_peer);
    if (sock_unix < 0) {
        printf("create domain sock failed!\n");
        return -1;
    }
    printf("create and bind unix socket down\n");

    int ret = connect_domain_sock(sock_unix, mysock_serv);
    if (ret < 0) {
        printf("connect sock failed!\n");
        close(sock_unix);
        unlink(mysock_peer);
        return -1;
    }
    printf("connect to unix socket down\n");

    send_fd(sock_unix, "", 1, fd);
    printf("sendmsg down\n");

    sleep(1);

    close(fd);
    close(sock_unix);
    unlink(mysock_peer);

    return 0;
}

参考

【Linux】消息传递的艺术:探索Linux消息队列机制
[Linux 进程间通信之消息队列:原理 + API 与实战 (System-V IPC)]( https://blog.csdn.net/weixin_56167571/article/details/147354341#:~:text=本文全面介绍了 Linux 下 System V ,%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E7%9A%84%E5%8E%9F%E7%90%86%E3%80%81%E5%87%BD%E6%95%B0%E7%94%A8%E6%B3%95%E5%8F%8A%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%EF%BC%8C%E5%8C%85%E6%8B%AC%20msgget%E3%80%81msgsnd%E3%80%81msgrcv%E3%80%81msgctl%20%E7%AD%89%E5%B8%B8%E7%94%A8%E6%8E%A5%E5%8F%A3%EF%BC%8C%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90%E4%BA%86%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E7%9A%84%E7%89%B9%E6%80%A7%E5%A6%82%E5%BC%82%E6%AD%A5%E9%80%9A%E4%BF%A1%E3%80%81%E7%B1%BB%E5%9E%8B%E8%BF%87%E6%BB%A4%E4%B8%8E%E6%8C%81%E4%B9%85%E6%80%A7%E7%AD%89%E3%80%82%20_linux%20%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%8E%9F%E7%90%86)

posted @ 2026-04-18 22:06  main_c  阅读(6)  评论(0)    收藏  举报