前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >15(进程间通信)

15(进程间通信)

作者头像
提莫队长
发布2019-02-21 11:10:48
5360
发布2019-02-21 11:10:48
举报
文章被收录于专栏:刘晓杰刘晓杰

本章讨论经典的IPC:管道、FIFO、消息队列、信号量以及共享存储器

1 管道

管道是Unix系统IPC最古老的方式。管道有下列两种局限性: (1) 历史上,它们是半双工的(即数据只能在一个方向上流动)。 (2) 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程就可以应用该管道

代码语言:javascript
复制
#include <unistd.h>
int pipe(int filedes[2]);               
            Returns: 0 if OK, 1 on error

经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。Filedes[1]的输出是filedes[0]的输入

这里写图片描述
这里写图片描述

经由管道从父进程向子进程传递数据

代码语言:javascript
复制
#include "apue.h"
Int main(void)
{
    int     n;
    int     fd[2];
    pid_t   pid;
    char    line[MAXLINE];

    if (pipe(fd) < 0)
        err_sys("pipe error");
    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid > 0) {       /* parent */
        close(fd[0]);
        write(fd[1], "hello world\n", 12);
    } else {                /* child */
        close(fd[1]);
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }
    exit(0);
}

popen和pclose函数

在管道操作中,常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其发送输入,所以标准I/O库为实现这些操作提供了两个函数POPEN和PCLOSE,这两个函数实现的操作是::

代码语言:javascript
复制
1创建一个管道
2FORK 一个子进程
3关闭管道的不是用端
4EXEC一个SHELL以执行命令
5等待命令终止
代码语言:javascript
复制
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
        Returns: file pointer if OK, NULL on error
int pclose(FILE *fp);
        Returns: termination status of cmdstring, or 1 on error
这里写图片描述
这里写图片描述

协同进程

当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。

这里写图片描述
这里写图片描述
代码语言:javascript
复制
#include "apue.h"
Int main(void)
{
    int     n,  int1,  int2;
    char    line[MAXLINE];

    while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
        line[n] = 0;        /* null terminate */
        if (sscanf(line, "%d%d", &int1, &int2) == 2) {
            sprintf(line, "%d\n", int1 + int2);
            n = strlen(line);
            if (write(STDOUT_FILENO, line, n) != n)
                err_sys("write error");
        } else {
            if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
                err_sys("write error");
        }
    }
    exit(0);
}

将此程序编译,把可执行目标代码存入名为add2的文件

使用add2的程序

代码语言:javascript
复制
#include "apue.h"
static void sig_pipe(int);      /* our signal handler */
Int main(void)
{
    int     n, fd1[2], fd2[2];
    pid_t   pid;
    char    line[MAXLINE];

    if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
        err_sys("signal error");

    if (pipe(fd1) < 0 || pipe(fd2) < 0)
        err_sys("pipe error");

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid > 0) {                         /* parent */
        close(fd1[0]);
        close(fd2[1]);
        while (fgets(line, MAXLINE, stdin) != NULL) {
            n = strlen(line);
            if (write(fd1[1], line, n) != n)
                err_sys("write error to pipe");
            if ((n = read(fd2[0], line, MAXLINE)) < 0)
                err_sys("read error from pipe");
            if (n == 0) {
                err_msg("child closed pipe");
                break;
            }
            line[n] = 0;    /* null terminate */
            if (fputs(line, stdout) == EOF)
                err_sys("fputs error");
        }
        if (ferror(stdin))
            err_sys("fgets error on stdin");
        exit(0);
    } else {                                  /* child */
        close(fd1[1]);
        close(fd2[0]);
        if (fd1[0] != STDIN_FILENO) {
            if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
                err_sys("dup2 error to stdin");
            close(fd1[0]);
        }

        if (fd2[1] != STDOUT_FILENO) {
            if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
                err_sys("dup2 error to stdout");
            close(fd2[1]);
        }
        if (execl("./add2", "add2", (char *)0) < 0)
            err_sys("execl error");
    }
    exit(0);
}
static void sig_pipe(int signo)
{
    printf("SIGPIPE caught\n");
    exit(1);
}

在程序中创建两个管道,父子进程各自关闭它们不需要的端口。两个管道一个用作协同进程的标准输入,另一个做标准输出。子进程调用dup2使管道描述符移至其标准输入和输出,然后调用execl

2 FIFO

创建FIFO类似于创建文件。确实,FIFO的路径名存在于文件系统中。

代码语言:javascript
复制
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
            Returns: 0 if OK, 1 on error 

Mkfifo函数中的mode参数的规格说明与open函数中的mode相同。一旦用mkfifo创建了FIFO,就可以用open打开。其实,一般的文件IO都可以用于FIFO FIFO有两种用途 (1)由 shell 命令使用以便将数据从一条管道线传送到另一条,为此无需创建临时文件 (2)用于 client-server 进程应用程序中,以在 client 和 server 间传递数据

3 消息队列

每个队列的struct msqid_ds结构如下:

代码语言:javascript
复制
struct msqid_ds {
     struct ipc_perm  msg_perm;     /* see Section 15.6.2 */
     msgqnum_t        msg_qnum;     /* # of messages on queue */
     msglen_t         msg_qbytes;   /* max # of bytes on queue */
     pid_t            msg_lspid;    /* pid of last msgsnd() */
     pid_t            msg_lrpid;    /* pid of last msgrcv() */
     time_t           msg_stime;    /* last-msgsnd() time */
     time_t           msg_rtime;    /* last-msgrcv() time */
     time_t           msg_ctime;    /* last-change time */
     .
     .
     .
   };

打开一个现存的队列或者创建一个新队列–msgget

代码语言:javascript
复制
#include <sys/msg.h>
int msgget(key_t key, int flag);
            Returns: message queue ID if OK, 1 on error

队列的设定 – msgctl msgctl 函数可以执行多种操作,与 ioctl、semctl、shmctl 非常类似,都被称为“垃圾桶函数 int msgctl(int msqid, int cmd, struct msqid_ds *buf); 调用成功返回 0,否则返回 -1 cmd 参数说明

代码语言:javascript
复制
IPC_STAT    取出该队列描述结构,并存放在 buf 指向的结构中
IPC_SET     按buf指向的结构中的值,设置队列描述结构中的msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_qbytes,但是要求执行该函数进程的有效用户ID必须等于msg_perm.cuid或msg_perm.uid,或该进程的有效用户是超级用户,并且只有超级用户才能增加msg_qbytes的值
IPC_RMID    从系统中删除该消息队列及队列中的所有数据,该操作会立即生效,取数据的进程接下来会立即返回EIDRM,与IPC_SET一样,该操作要求执行该函数进程的有效用户ID必须等于msg_perm.cuid或msg_perm.uid,活该进程的有效用户是超级用户

将数据加入队列 – msgsnd

代码语言:javascript
复制
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
调用成功返回 0,否则返回 -1

从队列中取出消息 – msgrcv

代码语言:javascript
复制
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
Returns: size of data portion of message if OK, 1 on error

参数说明 ptr—返回的消息存储在 ptr 指向的缓冲区中,缓冲区开始部分的长整型标识实际数据的大小 type—

代码语言:javascript
复制
type == 0   返回队列中的第一个消息(先进先出)
type > 0    返回队列中消息类型为 type 的消息
type < 0    返回队列中消息类型值不大于 type 绝对值的消息中类型值最小的一个消息

flag—

代码语言:javascript
复制
MSG_NOERROR     如果消息大于缓冲区,则被截断并正确返回,如果没有设置,则函数返回 E2BIG 错误信息
IPC_NOWAIT      操作不阻塞,如果队列为空则直接返回-1,errno设置为ENOMSG,如果没有设置IPC_NOWAIT则等待直到队列中有数据写入(如果在等待过程中队列被删除,则返回EINTR)

4 信号量

信号量与其他的 IPC (管道、FIFO、消息队列、域套接字)都有所不同,他是一个计数器,用于多进程对共享数据对象的访问 当进程需要获得共享资源时,需要进行下列操作:

这里写图片描述
这里写图片描述

当然了,对信号量的测试及减 1 的操作必须是原子操作,因此,通常信号量是内核实现的 最常用的信号量初始值为 1,被称为“二元信号量”或“双态信号量”,控制单个资源,但是一般而言,信号量的初始值可以为任意正数,用来说明有多少个共享资源单位可供共享应用 信号量集结构 – semid_ds

代码语言:javascript
复制
struct semid_ds
{
    struct ipc_perm sem_perm;   // 信号量集权限结构
    unsigned short  sem_nsems;  // 信号量集中的信号量数目
    time_t          sem_otime;  // 上次操作时间
    time_t          sem_ctime;  // 上次改变时间
    ...
}

信号量结构

代码语言:javascript
复制
struct
{
    unsigned short  semval;     // 信号量的值
    pid_t           sempid;     // 最后操作该信号量的进程 ID
    unsigned short  semncnt;    // 因为信号量值过大而等待的进程数
    unsigned short  semzcnt;    // 因为信号量值等于 0 而等待的进程数
    ...
}

信号量的创建与获取 – semget

代码语言:javascript
复制
int semget(key_t key, int nsems, int flag);
调用成功返回信号量集 ID,否则返回 -1

该函数将 key 变换为信号量集标识符,并返回,与消息队列创建函数 msgget 一样: 如果 key 取值为 IPC_PRIVATE,则创建新的 IPC 结构 如果指定的 key 当前未与任何 IPC 结构结合,并且 flag 中指定了 IPC_CREAT 位,则用该 key 创建新的 IPC 结构 如果指定的 key 当前未与任何 IPC 结构结合,并且 flag 中未指定 IPC_CREAT 位,则函数返回出错 如果指定的 key 当前已经与 IPC 结构结合,并且 flag 中未指定 IPC_EXECL 位,则返回对应 IPC 结构,否则返回 EEXIST 参数 nsems 用于初始化该信号量集描述结构的 sem_nsems 字段

信号量集的设定 – semctl semctl 函数可以执行多种操作,与 ioctl、semctl、shmctl 非常类似,都被称为“垃圾桶函数”

代码语言:javascript
复制
int semctl(int semid, int semnum, int cmd [,  semun arg ]);
根据 cmd 参数的不同,返回值有所不同

信号量操作函数 – semop 函数 semop 执行信号量集上的操作,即本文开头处图片中的过程,是一个原子操作

代码语言:javascript
复制
int semop(int semid, struct sembuf semoparray[], size_t nops);

参数说明 – sembuf 结构数组 semoparray 参数 semoparray 是一个信号量操作数组,标识对每个信号量的操作 struct sembuf { unsigned short sem_num; // 该操作对应信号量集中信号量编号 short sem_op; // 指定对信号量的操作 int sem_flg; // 信号量操作标志,可选 IPC_NOWAIT、SEM_UNDO }

5 共享存储器

共享内存允许两个或更多个进程共享一个给定的存储区域,这是最快的一种 IPC 但是,当某个进程正在读写共享存储的某个区域时,其他进程同时不应该对该区域进行操作,信号量通常被用来实现对共享存储访问的同步,当然,记录所也可以用于这种场合,但是记录锁在时间上要比信号量多消耗约 60% 内核为每个共享存储段设置了一个 shmid_ds 结构

代码语言:javascript
复制
struct shmid_ds
{
    struct ipc_perm shm_perm;   // 权限结构
    size_t          shm_segsz;  // 存储段大小(字节数)
    pid_t           shm_lpid;   // 最后一个操作共享内存段的进程 ID
    pid_t           shm_cpid;   // 创建共享内存段的进程 ID
    shmatt_t        shm_nattch; // 连接计数
    time_t          shm_atime;  // 最后执行 attach 操作的时间
    time_t          shm_dtime;  // 最后执行 detach 操作的时间
    time_t          shm_ctime;  // 最后执行 change 操作的时间
    ...
}

共享存储的创建与获取 – shmget 函数 shmget 用于创建一个新的共享存储段或获取一个已经存在的共享存储段 int shmget(key_t key, size_t size, int flag);

共享存储的设定 – shmctl shmctl 函数可以执行多种操作,与 ioctl、semctl、shmctl 非常类似,都被称为“垃圾桶函数”

代码语言:javascript
复制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

cmd 参数可以指定下列 5 种命令的一种

代码语言:javascript
复制
IPC_STAT    获取shmid对应的存储段描述结构shmid_ds并存储在参数buf所指向的内存中
IPC_SET     按buf所指向内存中的shmid_ds结构设置shmid对应的存储段描述结构(可以改变shm_perm.uid、shm_perm.gid、shm_perm.mode),执行此命令的进程有效用户ID必须等于sem_perm.cuid或sem_perm.uid或者该进程具有超级用户权限
IPC_RMID    从系统中删除该共享存储段,与其他两个XSI IPC的相应操作不同,执行后并不会立即删除,除非该存储段的最后一个进程终止或与该段脱离连接(shm_nattch值变为0),执行此命令的进程有效用户ID必须等于sem_perm.cuid或sem_perm.uid或者该进程具有超级用户权限
SHM_LOCK    将共享存储段锁定在内存中,只有超级用户可以执行此命令
SHM_UNLOCK  解锁共享内存段,只有超级用户可以执行此命令

连接共享存储到地址空间 – shmat 一旦创建了一个共享存储段,进程就可以通过调用 shmat 函数将它连接到它的地址空间中:

代码语言:javascript
复制
void *shmat(int shmid, const void *addr, int flag);

addr 参数说明

  1. 若 addr 为 0,则此段连接到有内核选择的第一个可用地址上(推荐使用)
  2. 若 addr 不为 0,则需要参考 flag 参数的值

flag 参数说明

  1. SHM_RND 连接到 addr 最近的一个 2 的乘方地址上(若未指定该标识,则直接连接到 addr 指向的地址)
  2. SHM_RDONLY 连接后,该段只能被以只读方式使用(若未指定该标志,则以读写方式使用)

共享存储到地址空间的脱接 – shmdt 当对共享存储段的操作结束时,调用shmdt脱接该段 int shmdt(void *addr); shmdt 的实际操作是将 shm_nattch 的值减 1

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年04月25日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 管道
  • popen和pclose函数
  • 协同进程
  • 2 FIFO
  • 3 消息队列
  • 4 信号量
  • 5 共享存储器
相关产品与服务
消息队列 CMQ 版
消息队列 CMQ 版(TDMQ for CMQ,简称 TDMQ CMQ 版)是一款分布式高可用的消息队列服务,它能够提供可靠的,基于消息的异步通信机制,能够将分布式部署的不同应用(或同一应用的不同组件)中的信息传递,存储在可靠有效的 CMQ 队列中,防止消息丢失。TDMQ CMQ 版支持多进程同时读写,收发互不干扰,无需各应用或组件始终处于运行状态。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档