管道

匿名管道

匿名管道 (pipe) 是一种半双工的通信方式,里面的数据只能单向流动,且只能在具有亲缘关系的进程中使用。进程的亲缘关系通常指父子进程关系

匿名管道在 C 语言中由 pipe() 函数创建:

#include <unistd.h>    // 包含 pipe() 函数的头文件

/*
 * 参数 filedis 返回两个文件描述符: 
 * filedis[0] 为读数据打开, filedis[1] 为写数据打开
 * filedis[1] 的输出即为 filedis[0] 的输入
 */ 
int pipe(int filedis[2]);

上管道比较灵活,能看到管道所 对应的文件描述符的进程之间都可以使用,但是务必注意同步关系。另外用户要关闭不使用的 管道端口,否则可能会出现异常情况。C 库中的用户态函数 popen() 和 pclose() 对 pipe() 系统调 用进行了封装,更方便和安全。

下面是父子进程间通讯的例子:

void anonymousPipe()
{
    int pipes[2];
    char data[] = "Hello world!";
    char buffer[BUFSIZ + 1];
    pid_t forkResult;

    memset(buffer, '\0', sizeof(buffer));

    // create anonymous pipe
    if (pipe(pipes) == 0)
    {
        forkResult = fork();

        if (forkResult == -1)
        {
            fprintf(stderr, "Fork fail");
            exit(EXIT_FAILURE);
        }

        // if forkResult equals zero, we're in the child progress
        if (forkResult == 0)
        {
            int readSize = read(pipes[0], buffer, BUFSIZ);
            printf("Read %d bytes: %s\n", readSize, buffer);
            exit(EXIT_SUCCESS);
        }
        else // otherwise, we're in the parent progress
        {
            int writeSize = write(pipes[1], data, strlen(data));
            printf("Wrote %d bytes\n", writeSize);
            exit(EXIT_SUCCESS);
        }
    }

    exit(EXIT_SUCCESS);
}

有名管道

有名管道 (popen) 也是一种半双工的通信方式,但是它允许无亲缘关系的进程进行通信,而不需要有一个共同的祖先进程。

有名管道是基于 FIFO 文件来完成的,是一种特殊类型的文件。它在系统文件中以文件名的形式存在,行为和无名管道 (pipe) 相似。

创建有名管道

C 语言中有名管道可以通过以下两个不同的函数进行调用:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
int mknod(const char *pathname, mode_t mode | S_FIFO, (dev_t)0);

创建了有名管道后,就可以使用一般的文件 I/O 函数进行操作。如 open, close, read, write

创建有名管道示例代码:

void createFIFO()
{
    int res = mkfifo("/tmp/tmpFIFO", 0777);

    if (res == 0)
        printf("FIFO created\n");
    else
        printf("Return code is %d\n", res);

    exit(EXIT_SUCCESS);
}
这里我遇到一个小问题。我是在 WSL 上面运行的程序,所以当前目录是 Windows 里面的磁盘。创建 FIFO 文件的时候提示 Operation not permitted ,切换到 Linux 里面的磁盘就可以正常创建。具体原因未知,推测是 Windows 磁盘在 WSL 里面是挂载状态导致的。

访问 FIFO 文件

与通过pipe调用创建管道不同,FIFO是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读写操作之前必须先打开它。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);

参数

  • pathname: 文件路径
  • flags:

    flag作用
    O_RDONLYopen将会调用阻塞,除非有另外一个进程以的方式打开同一个FIFO,否则一直等待,不会返回。
    O_RDONLY \O_NONBLOCK即使此时没有其他进程以的方式打开FIFO,此时open也会成功并立即返回,此时FIFO被读打开,而不会返回错误。
    O_WRONLYopen将会调用阻塞,除非有另外一个进程以的方式打开同一个FIFO,否则一直等待。
    O_WRONLY \O_NONBLOCK该函数的调用总是立即返回,但如果此时没有其他进程以的方式打开,open调用将返回-1,并且此时FIFO没有被打开。如果缺少有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。
    FIFO 文件不能以 O_RDWR 模式打开,这样的行为是未定义的。通常的 FIFO 文件都是单向的,所以没有必要使用这个模式。如果一个管道以读/写模式打开,进程就会从这个管道读回自己的输出。

消息队列

消息队列就是一个消息的链表,可以把消息看作一个记录,具有特定的格式以及特定的优先级。一个有写权限的进程按照一定的规则对消息队列进行信息添加,对消息队列有读权限的进程则可以从消息队列中读走消息,从而实现进程间的通信。

创建或获取消息队列

#include <sys/msg.h>  // 头文件
#include <sys/types.h>
#include <sys/ipc.h>
/* 
 * key: 消息队列的标识, 用来命名某个特定的消息队列
 * msgflg: 这个参数有两种值
 *    IPC_CREAT: 若没有该队列, 则创建一个并返回新标识符; 若存在则返回原标识符
 *    IPC_EXCL: 若没有该队列, 则返回 -1 ; 若存在则返回 0 
 */
int msgget(key_t key, int msgflg);

向队列读/写消息

int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:

  • msqid: 消息队列标识码
  • msgp: 指向消息缓冲区的指针,用来暂时存储发送和接受的消息,是一个用户可定义的通用结结构,举例如下:

    struct msgstru
    {
        long mtype;            // 消息类型, 必须 > 0
        char mtext[N];        // 消息文本, N 为任意整数
    }
  • msgsz: 消息的长度,注意不是结构体的大小
  • msgtyp: 从消息队列内读取的消息形态。若值为零,则表示消息队列中所有消息都会被读取
  • msgflg: 控制队列中没有对应类型消息时的处理逻辑。

    对于 msgsnd 函数,msgflg 控制着当前消息队列满或消息队列到达系统范围的限制时将要发生的事情。如果 msgflg 设置为IPC_NOWAIT,则在 msgsnd() 执行时若是消息队列已满,则 msgsnd() 将不会阻塞,不会发送信息,立即返回-1。如果执行的是 msgrcv() ,则在消息队列呈空时,不做等待马上返回-1,并设定错误码为ENOMSG。当 msgflg 为 0 时,msgsnd() 及 msgrcv() 在队列呈满或呈空的情形时,采取阻塞等待的处理模式。此时对于发送进程而已发送进程将挂起以等待队列中腾出可用的空间;对于接收进程而言,该进程将会挂起以等待一条相应类型的信息到达。

设置消息队列属性

int msgctl(int msgqid, int cmd, struct msqid_ds *buf)

参数

  • msgctl 系统调用对 msgqid 标识的消息队列执行 cmd 操作,系统定义了 3 种 cmd 操作: IPC_STAT , IPC_SET , IPC_RMID
  • IPC_STAT : 该命令用来获取消息队列对应的 msqid_ds 数据结构,并将其保存到 buf 指定的地址空间。
  • IPC_SET : 该命令用来设置消息队列的属性,要设置的属性存储在buf中。
  • IPC_RMID : 从内核中删除 msqid 标识的消息队列。

踩过的坑

查看消息队列: ipcs -q

Function not implemented : 在 WSL 上是不能用消息队列的。出现 这个错误就检查一下是不是在 WSL 上面跑的吧

Argument list too long : 注意 msgrcv 里面的length不能比你用 msgsnd 传进来的长度要短,否则就会报这个错误。

比如:如果你 msgsnd 中 length 用 sizeof(msgbuf)-sizeof(long) 传的,而你的数据只有一个字节,这样在 msgrcv 中用 sizeof(msgbuf.data) 的话就会因为你的缓冲区不够而报错

共享内存

共享内存是最快的IPC(进程间通信)方式,它允许两个不相关进程访问同一个逻辑内存。共享内存是一个程序向内存写数据,另一个程序读数据,共享内存牵扯到同步的问题,一般有三种方案可以实现共享资源的同步。它们分别是信号量,记录锁和互斥量

  • 信号量: 首先服务端创建一个只含一个信号的信号量集合,并初始化为1。若要占据资源,则以 sem_op=-1 调用 semop 函数。若要释放资源,则以 sem_op=1 调用 semop 函数。
  • 记录锁: 需要创建一个文件,并写入一个字节。若要分配资源,对文件获得写锁;若要释放资源,则解锁。
  • 互斥量: 需要所有的进程将相同的文件映射到他们的地址空间,并使用 PTHREAD_PROCESS_SHARED 互斥量属性在文件中初始化互斥量。若要分配资源,对互斥量加锁;若要释放资源,解锁互斥量。

共享内存常用的 C 函数

#include <sys/shm.h>  // 头文件

shmget 函数

用于开辟或指向一块共享内存,返回获得共享内存区域的ID(共享内存标识符),该标识符用于后续的共享内存函数。如果不存在指定的共享区域就创建相应的区域。 如果创建失败,则返回-1。

原型如下:

int shmget(key_t key, size_t size, int shmflg); 

参数:

  • key: 共享内存的标识符。如果是父子关系的进程间通信的话,这个标识符用 IPC_PRIVATE 来代替。如果两个进程没有任何关系,用 ftok() 算出来一个标识符(或者自己定义一个)使用即可。
  • size: 以字节为单位指定需要共享的内存容量。
  • shmflg: 包含 9 个 bit 的权限标志,它是这块内存的模式 (mode) 以及权限标识

    如果要想在 key 标识的共享内存不存在时,创建它的话,可以与 IPC_CREAT 做或操作。若 key 存在共享内存的话,该标识会被忽略。

    权限标志的存在,使得允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,同时其他用户创建的进程只能读取该共享内存。可以利用这个功能提高一种有效的对数据进行只读访问的方法,通过将数据放入共享内存并设置它的权限,即可避免数据被其他用户修改。

    举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

返回值:

函数调用成功时返回共享内存的ID,失败时返回-1。

shmat 函数

第一次创建完共享内存时,它还不能被任何进程访问,shmat 函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。

原型如下:

void* shmat(int shm_id,  const void *shm_addr, int shmflg); 

参数:

  • shm_id: 共享内存标识,可由 shmget 函数返回值得到。
  • shm_addr: 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
  • shm_flg: 本进程对该内存的操作模式,可以由两个取值: SHM_RND SHM_RDONLYSHM_RND 为读写模式;
    SHM_RDONLY 是只读模式。需要注意的是,共享内存的读写权限由它的属主、它的访问权限和当前进程的属主共同决定。如果当 shmflg & SM_RDONLY 为true时,即使该共享内存的访问权限允许写操作,它也不能被写入。该参数通常会被设为0。

返回值:

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

shmdt 函数

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。

原型如下:

int shmdt(const void *shmaddr);

参数:

  • shmaddr: 共享内存的起始地址。可由 shmat 函数返回值获得。

返回值:

函数调用成功时返回0,失败时返回-1。

shmctl 函数

与信号量的 semctl 函数一样,用来控制共享内存。

原型如下:

int shmctl(int shm_id, int command, struct shmid_ds *buf);

参数:

  • shm_id: 共享内存标识,可由 shmget 函数返回值得到。
  • command: 要采取的操作,可以取以下三个值:

    • IPC_STAT: 把 shmid_ds 结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖 shmid_ds 的值。
    • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    • IPC_RMID:删除共享内存段
  • buf: 一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定。

shmid_ds 结构至少包含以下成员:

struct shmid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    uid_t shm_perm.mode;
}

返回值

函数调用成功时返回0,失败时返回-1。

踩坑记录

若出现 undefined reference to 'sem_open' 错误,则表示链接的时候没有加上多线程的参数。我在 cmakelist 加上 set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread" ) 后解决。

最后修改:2020 年 04 月 25 日
如果觉得我的文章对你有用,请随意赞赏