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]的输入

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

#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

协同进程

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

#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、消息队列、域套接字)都有所不同,他是一个计数器,用于多进程对共享数据对象的访问

当进程需要获得共享资源时,需要进行下列操作:

当然了,对信号量的测试及减 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

时间: 2024-11-08 21:45:03

15(进程间通信)的相关文章

Boost 15 进程间通信

1. 介绍 Boost.Interprocess库简化了使用通用的进程间通信和同步机制.并且提供这些机制的部件: * 共享内存 * 内存映射文件 * 信号量,互斥量,条件变量和可升级的互斥量类型,该类型可以放入共享内存和内存映射文件中 * 命名版本的同步对象 * 文件锁 * 相对指针 * 消息队列 Boost.Interprocess还提供了更高级的进程间机制,用于动态分配共享内存或者内存映射文件.: * 在共享内存或者文件映射中动态创建匿名或者命名对象. * 与共享内存或者文件映射兼容的类似S

进程间通信 详解

进程间通信(IPC)介绍 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息. IPC的方式通常有管道(包括无名管道和命名管道).消息队列.信号量.共享存储.Socket.Streams等.其中 Socket和Streams支持不同主机上的两个进程IPC. 以Linux中的C语言编程为例. 一.管道 管道,通常指无名管道,是 UNIX 系统IPC最古老的形式. 1.特点: 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端.

进程间通信(五)—信号

我会用几篇博客总结一下在Linux中进程之间通信的几种方法,我会把这个开头的摘要部分在这个系列的每篇博客中都打出来 进程之间通信的方式 管道 消息队列 信号 信号量 共享存储区 套接字(socket) 进程间通信(三)—信号量传送门:http://www.cnblogs.com/lenomirei/p/5649792.html 进程间通信(二)—消息队列传送门:http://www.cnblogs.com/lenomirei/p/5642575.html 进程间通信(一)—管道传送门:http:

Android Binder进程间通信---ServiceManager代理对象的获取过程

本文参考<Android系统源代码情景分析>,作者罗升阳. 一.测试代码: -/Android/external/binder/server ----FregServer.cpp ~/Android/external/binder/common ----IFregService.cpp ----IFregService.h ~/Android/external/binder/client ----FregClient.cpp Binder库(libbinder)代码: ~/Android/fr

进程间通信之信号量

一.对信号量的理解 信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识. 当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用.大于0,资源可以请求,小于0,无资源可用,进程会进入睡眠状态直至资源可用.当进程不再使用一个信号量控制的共享资源时,信号量的值+1,对信号量的值进行的增减操作均为原子操作. 二.使用信号量的原因 防止出现因多个程序同时访问一个共享资源时而引发的一系

Android进程间通信之使用AIDL

AIDL(Android Interface Definition Language ),可实现进程间的通信,并且允许多线程访问.(如果需要进程间通信,又不需要处理多线程访问,那么使用Messenger的方式更为合适),实现AIDL,需要以下几个步奏. 1.定义AIDL接口 AIDL接口使用后缀名为.aidl的文件来定义(例如创建一个IRemoteData.aidl),使用java语法来编写,在编译时,Android sdk工具会在/gen目录下生成一个java文件(IRemoteData.ja

〖Linux〗Linux高级编程 - 进程间通信(Interprocess Communication)

[转自: http://blog.csdn.net/Paradise_for_why/article/details/5550619] 这一章就是著名的IPC,这个东西实际的作用和它的名字一样普及.例如我们浏览网页,打印文章,等等. IPC总共有五种类型: 共享内存(Shared Memory):最容易理解的一种,就像一个特工把情报放在特定地点(内存),另一个特工再过来取走一样. 内存映射(Mapped Memory):和共享内存几乎相同,除了特工们把地点从内存改成了文件系统. 管道(Pipes

从并发处理谈PHP进程间通信(二)System V IPC

.container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .container::before,.container::after { content: " "; display: table } .container::after { clear: both } .container::before,.container::after { content:

linux进程间通信之消息队列

我们已经知道进程通信的方式是有多种的,在上一篇博客中讲述了通过管道实现简单的进程间通信,那么接下来我们看看与之类似的另一种方式,通过消息队列来实现进程间通信. 什么是消息队列 消息队列提供了一种由一个进程向另一个进程发送块数据的方法.另外,每一个数据块被看作有一个类型,而接收进程可以独立接收具有不同类型的数据块.消息队列的好处在于我们几乎可以完全避免同步问题,并且可以通过发送消息屏蔽有名管道的问题.更好的是,我们可以使用某些紧急方式发送消息.坏处在于,与管道类似,在每一个数据块上有一个最大尺寸限