15(进程间通信)

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

1 管道

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

#include <unistd.h>
int pipe(int filedes[2]);               
            Returns: 0 if OK, 1 on error

经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。Filedes[1]的输出是filedes[0]的输入
15(进程间通信)_第1张图片
经由管道从父进程向子进程传递数据

#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,这两个函数实现的操作是::

1创建一个管道
2FORK 一个子进程
3关闭管道的不是用端
4EXEC一个SHELL以执行命令
5等待命令终止
#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

协同进程

当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。
15(进程间通信)_第2张图片

#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的程序

#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的路径名存在于文件系统中。

#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结构如下:

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

#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 参数说明

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

int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
调用成功返回 0,否则返回 -1

从队列中取出消息 – msgrcv

#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—

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

flag—

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

4 信号量

信号量与其他的 IPC (管道、FIFO、消息队列、域套接字)都有所不同,他是一个计数器,用于多进程对共享数据对象的访问
当进程需要获得共享资源时,需要进行下列操作:
15(进程间通信)_第3张图片
当然了,对信号量的测试及减 1 的操作必须是原子操作,因此,通常信号量是内核实现的
最常用的信号量初始值为 1,被称为“二元信号量”或“双态信号量”,控制单个资源,但是一般而言,信号量的初始值可以为任意正数,用来说明有多少个共享资源单位可供共享应用
信号量集结构 – semid_ds

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

信号量结构

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

信号量的创建与获取 – semget

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 非常类似,都被称为“垃圾桶函数”

int semctl(int semid, int semnum, int cmd [,  semun arg ]);
根据 cmd 参数的不同,返回值有所不同

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

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 结构

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 非常类似,都被称为“垃圾桶函数”

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

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

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 函数将它连接到它的地址空间中:

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

你可能感兴趣的:(15(进程间通信))