京东金融陈生强 简历:Linux面试题 Linux笔试题 介绍一下IPC:interprocess commun...

来源:百度文库 编辑:中财网 时间:2024/05/10 06:33:06

IPC:interprocess communication 进程间通信

 

一、管道:

 

1. 无名管道:

打开与关闭管道:

#include

int pipe(int filedes[2]);

filedes[0]用于读出数据,读取时必须关闭写入端,即close(filedes[1]);

filedes[1]用于写入数据,写入时必须关闭读取端,即close(filedes[0])。

2. 有名管道FIFO:

创建FIFO:

#include

#include

int mkfifo(const char *pathname, mode_t mode);

读写管道与读写文件的操作相同。

 

二、System V FIFO:

 

1. 共享内存:

创建共享内存区:

#include

#include

#include

int shmget(key_t key, int size, int flags);

附加/分离共享内存区:

#include

#include

#include

char *shmat(int shmid, char *shmaddr, int flags);

int shmdt(char *shmadr);

2. 消息队列:

创建消息队列:

#include

#include

#include

int msgget(key_t key, int flags);

读写消息:

#include

#include

#include

int msgsnd(int msqid, const void *prt, size_t nbytes, int flags);

int msgrcv(int msqid, void *prt, size_t nbytes, int flags);

prt是指向msgbuf结构的指针,msgbuf在中定义如下:

struct msgbuf {

long mtype;

char mtext[1];

};

msgbuf结构可以自由更改设置。

删除消息队列:

#include

#include

#include

int int msgctl(int msqid, int cmd, struct msqid_ds *buf);

cmd为IPC_RMID时为删除队列msqid。

3. 信号量:

创建/打开:

#include

#include

#include

int semget(key_t key, int nsems, int flags);

int semop(int semid, struct sembuf *semops, unsigned nops);

sembuf结构定义如下:

struct sembuf {

short sem_num; /* Semaphore number */

short sem_op; /* The operation to perform */

short sem_flg; /* Flags controlling the operation */

};

如果sem_op为正,表示资源被释放,信号量增加。

如果sem_op为负,表示资源被申请,信号量减少。

如果sem_op为0,表示进程被阻塞直到信号量变为0。

控制和删除:

#include

#include

#include

int semctl(int semid, int semnum, int cmd, union semun arg);

当cmd为IPC_RMID时为删除信号量集。

 

 

 

 

一个大型的应用系统,往往需要众多进程协作,进程(Linux进程概念见附1)间通信的重要性显而易见。本系列文章阐述了Linux环境下的几种主要进程间通信手段,并针对每个通信手段关键技术环节给出详细实例。为达到阐明问题的目的,本文还对某些通信手段的内部实现机制进行了分析。

 

 

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:

 

 

其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括: Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开发了一个独立的Unix标准,这个新的ANSI Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。

 

图一给出了linux 所支持的各种IPC手段,在本文接下来的讨论中,为了避免概念上的混淆,在尽可能少提及Unix的各个版本的情况下,所有问题的讨论最终都会归结到Linux环境下的进程间通信上来。并且,对于Linux所支持通信手段的不同实现版本(如对于共享内存来说,有Posix共享内存区以及System V共享内存区两个实现版本),将主要介绍Posix API。

 

linux下进程间通信的几种主要手段简介:

 

管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

下面将对上述通信机制做具体阐述。

 

附1:参考文献[2]中对linux环境下的进程进行了概括说明:

 

一般来说,linux下的进程包含以下几个关键要素:

 

有一段可执行程序;

有专用的系统堆栈空间;

内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;

具有独立的存储空间

进程和线程有时候并不完全区分,而往往根据上下文理解其含义。

 

Linux环境进程间通信 ——无名管道工作机制研究

 

一、引言

Linux作为一个开源的操作系统,是我们进行操作系统和提高编程水平的最佳途径之一。

 

好的程序如同好的音乐一样,完成的完美、巧妙。开放源码的程序都是经过无数人检验地,本文将以linux-kernel-2.6.5为例对pipe的工作机制进行阐述。

 

 

二、进程间通信的分类

大型程序大多会涉及到某种形式的进程间通信,一个较大型的应用程序设计成可以相互通信的“碎片”,从而就把一个任务分到多个进程中去。进程间通信的方法有三种方式:

 

管道(pipe)

 

套接字(socket)

 

System v IPC 机制

 

管道机制在UNIX开发的早期就已经提供了,它在本机上的两个进程间的数据传递表现的相当出色;套接字是在BSD(Berkeley Software Development)中出现的,现在的应用也相当的广泛;而System V IPC机制Unix System V 版本中出现的。

 

 

 

三、工作机制

 

管道分为pipe(无名管道)和FIFO(    命名管道),它们都是通过内核缓冲区按先进先出的方式数据传输,管道一端顺序地写入数据,另一端顺序地读入数据读写的位置都是自动增加,数据只读一次,之后就被释放。在缓冲区写满时,则由相应的规则控制读写进程进入等待队列,当空的缓冲区有写入数据或满的缓冲区有数据读出时,就唤醒等待队列中的写进程继续读写。

 

 

管道的读写规则:

 

管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等

 

四、pipe的数据结构

首先要定义一个文件系统类型:pipe_fs_type。

 

fs/pipe.c

static struct file_system_type pipe_fs_type = {

.name        = “pipefs”,

.get_sb        = pipefs_get_sb,

.kill_sb                    = kill_anon_super,

};

变量pipe_fs_type其类型是 struct file_system_type 用于向系统注册文件系统。

Pipe以类似文件的方式与进程交互,但在磁盘上无对应节点,因此效率较高。Pipe主要包括一个inode和两个file结构——分别用于读和写。Pipe的缓冲区首地址就存放在inode的i_pipe域指向pipe_inode_info结构中。但是要注意pipe的inode并没有磁盘上的映象,只在内存中交换数据。

 

static struct super_block *pipefs_get_sb(struct file_system_type *fs_type,

int flags, const char *dev_name, void *data)

{

return get_sb_pseudo(fs_type, “pipe:”, NULL, PIPEFS_MAGIC);

}

上为超级的生成函数。

Include/linux/pipe.h

#ifndef _LINUX_PIPE_FS_I_H

#define _LINUX_PIPE_FS_I_H

 

#define PIPEFS_MAGIC 0×50495045

struct pipe_inode_info {

 

wait_queue_head_t wait;         1

 

char *base;                     2

 

unsigned int len;               3

unsigned int start;             4

unsigned int readers;           5

unsigned int writers;           6

unsigned int waiting_writers;   7

unsigned int r_counter;         8

unsigned int w_counter;         9

struct fasync_struct *fasync_readers;   10

 

struct fasync_struct *fasync_writers;     11

 

};

2 管道等待队列指针wait

3 内核缓冲区基地址base

4 缓冲区当前数据量

6 管道的读者数据量

7 管道的写者数据量

8 等待队列的读者个数

9 等待队列的写者个数

11、12 主要对 FIFO

 

五、管道的创建:

通过pipe系统调用来创建管道。

int do_pipe(int *fd)

{

struct qstr this;

char name[32];

struct dentry *dentry;

struct inode * inode;

struct file *f1, *f2;

int error;

int i,j;

 

error = -ENFILE;

f1 = get_empty_filp();           //分配文件对象,得到文件对象指针用于读管道

if (!f1)

goto no_files;

 

f2 = get_empty_filp();          //分配文件对象,得到文件对象指针用于读管道

if (!f2)

goto close_f1;

 

inode = get_pipe_inode();    //调用get_pipe_inode获得管道类型的索引节点

if (!inode)                              的指针inode。

goto close_f12;

 

error = get_unused_fd();      //获得当前进程的两个文件描述符。在当前的

if (error < 0)                          进程的进程描述符file域中,有一个fd 域,

goto close_f12_inode;    //指向该进程当前打开文件指针数组,数组

i=error;                                           元素是指向文件对象的指针。

 

error = get_unused_fd();

if (error < 0)

goto close_f12_inode_i;

j = error;

 

 

 

error = -ENOMEM;

sprintf(name, “[%lu]“, inode->i_ino);       //生成对象目录dentry,

this.name = name;                                  并通过它将上述两个文

this.len = strlen(name);                           件对象将的指针与管道

this.hash = inode->i_ino; /* will go */        索引节点连接起来。

dentry = d_alloc(pipe_mnt->mnt_sb->s_root, &this);

if (!dentry)

goto close_f12_inode_i_j;

dentry->d_op = &pipefs_dentry_operations;

d_add(dentry, inode);

f1->f_vfsmnt = f2->f_vfsmnt = mntget(mntget(pipe_mnt));

f1->f_dentry = f2->f_dentry = dget(dentry);

f1->f_mapping = f2->f_mapping = inode->i_mapping;

 

/* read file */

f1->f_pos = f2->f_pos = 0;                  //为用于读的两个文件对象设置属性值

f1->f_flags = O_RDONLY;                       f_flage设置为只读,f_op设置为

f1->f_op = &read_pipe_fops;                  read_pipe_fops 结构的地址。

f1->f_mode = 1;

f1->f_version = 0;

 

/* write file */                                   //为用于写的两个文件对象设置属性值

f2->f_flags = O_WRONLY;                     f_flage设置为只写,f_op设置为

write_pipe_fops 结构的地址。

f2->f_op = &write_pipe_fops;

f2->f_mode = 2;

f2->f_version = 0;

 

fd_install(i, f1);

fd_install(j, f2);

fd[0] = i;                                        //将两个文件描述符放入参数fd数组返回

fd[1] = j;

return 0;

 

close_f12_inode_i_j:

put_unused_fd(j);

close_f12_inode_i:

put_unused_fd(i);

close_f12_inode:

free_page((unsigned long) PIPE_BASE(*inode));

kfree(inode->i_pipe);

inode->i_pipe = NULL;

iput(inode);

close_f12:

put_filp(f2);

close_f1:

put_filp(f1);

no_files:

return error;

}

 

六、管道的释放

管道释放时f-op的release域在读管道和写管道中分别指向pipe_read_release()和pipe_write_release()。而这两个函数都调用release(),并决定是否释放pipe的内存页面或唤醒该管道等待队列的进程。

以下为管道释放的代码:

static int pipe_release(struct inode *inode, int decr, int decw)

{    down(PIPE_SEM(*inode));

PIPE_READERS(*inode) -= decr;

PIPE_WRITERS(*inode) -= decw;

if (!PIPE_READERS(*inode) && !PIPE_WRITERS(*inode)) {

struct pipe_inode_info *info = inode->i_pipe;

inode->i_pipe = NULL;

free_page((unsigned long) info->base);

kfree(info);

} else {        wake_up_interruptible(PIPE_WAIT(*inode));

kill_fasync(PIPE_FASYNC_READERS(*inode), SIGIO, POLL_IN);

kill_fasync(PIPE_FASYNC_WRITERS(*inode), SIGIO, POLL_OUT);    }

up(PIPE_SEM(*inode));

return 0;}

 

七、管道的读写

1.从管道中读取数据:

如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;

 

当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。

 

2.向管道中写入数据:

向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞

 

八、管道的局限性

管道的主要局限性正体现在它的特点上:

 

只支持单向数据流;

 

只能用于具有亲缘关系的进程之间;

 

没有名字;

 

管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);

 

管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等。

 

 

 

九、后记

 

写完本文之后,发现有部分不足之处。在由于管道读写的代码过于冗长,限于篇幅不一一列出。有不足和错误之处还请各位老师指正。通过一段时间对Linux的内核代码的学习,开源的程序往往并非由“权威人士”、“享誉海内外的专家”所编写,它们的由一个个普通的程序员写就。但专业造就专家,长时间集中在某个领域中能够创建出据程序员应该珍视的财富

 

 

 

深刻理解Linux进程间通信(IPC)

一个大型的应用系统,往往需要众多进程协作,进程(Linux进程概念见附1)间通信的重要性显而易见。本系列文章阐述了Linux环境下的几种主要进程间通信手段,并针对每个通信手段关键技术环节给出详细实例。为达到阐明问题的目的,本文还对某些通信手段的内部实现机制进行了分析。

 

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:

 

其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括: Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开发了一个独立的Unix标准,这个新的ANSI Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。

 

图一给出了linux 所支持的各种IPC手段,在本文接下来的讨论中,为了避免概念上的混淆,在尽可能少提及Unix的各个版本的情况下,所有问题的讨论最终都会归结到Linux环境下的进程间通信上来。并且,对于Linux所支持通信手段的不同实现版本(如对于共享内存来说,有Posix共享内存区以及System V共享内存区两个实现版本),将主要介绍Posix API。

 

linux下进程间通信的几种主要手段简介:

 

管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

下面将对上述通信机制做具体阐述。

 

附1:参考文献[2]中对linux环境下的进程进行了概括说明:

 

一般来说,linux下的进程包含以下几个关键要素:

 

有一段可执行程序;

有专用的系统堆栈空间;

内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;

具有独立的存储空间

进程和线程有时候并不完全区分,而往往根据上下文理解其含义。

 

 

进程与线程的区别

进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务。

 

那进程与线程的区别到底是什么?进程是执行程序的实例。例如,当你运行记事本程序(Nodepad)时,你就创建了一个用来容纳组成Notepad.exe的代码及其所需调用动态链接库的进程。每个进程均运行在其专用且受保护的地址空间内。因此,如果你同时运行记事本的两个拷贝,该程序正在使用的数据在各自实例中是彼此独立的。在记事本的一个拷贝中将无法看到该程序的第二个实例打开的数据。

 

以沙箱为例进行阐述。一个进程就好比一个沙箱。线程就如同沙箱中的孩子们。孩子们在沙箱子中跑来跑去,并且可能将沙子攘到别的孩子眼中,他们会互相踢打或撕咬。但是,这些沙箱略有不同之处就在于每个沙箱完全由墙壁

和顶棚封闭起来,无论箱中的孩子如何狠命地攘沙,他们也不会影响到其它沙箱中的其他孩子。因此,每个进程就象一个被保护起来的沙箱。未经许可,无人可以进出。

 

实际上线程运行而进程不运行。两个进程彼此获得专用数据或内存的唯一途径就是通过协议来共享内存块。这是一种协作策略。下面让我们分析一下任务管理器里的进程选项卡。

 

这里的进程是指一系列进程,这些进程是由它们所运行的可执行程序实例来识别的,这就是进程选项卡中的第一列给出了映射名称的原因。请注意,这里并没有进程名称列。进程并不拥有独立于其所归属实例的映射名称。换言之

,如果你运行5个记事本拷贝,你将会看到5个称为Notepad.exe的进程。它们是如何彼此区别的呢?其中一种方式是通过它们的进程ID,因为每个进程都拥有其独一无二的编码。该进程ID由Windows NT或Windows 2000生成,并可以循环使用。因此,进程ID将不会越编越大,它们能够得到循环利用。第三列是被进程中的线程所占用的CPU时间百分比。它不是CPU的编号,而是被进程占用的CPU时间百分比。此时我的系统基本上是空闲的。尽管系统看上去每一秒左右都只使用一小部分CPU时间,但该系统空闲进程仍旧耗用了大约99%的CPU时间。

 

第四列,CPU时间,是CPU被进程中的线程累计占用的小时、分钟及秒数。请注意,我对进程中的线程使用占用一词。这并不一定意味着那就是进程已耗用的CPU时间总和,因为,如我们一会儿将看到的,NT计时的方式是,当特定的时钟间隔激发时,无论谁恰巧处于当前的线程中,它都将计算到CPU周期之内。通常情况下,在大多数NT系统中,时钟以10毫秒的间隔运行。每10毫秒NT的心脏就跳动一下。有一些驱动程序代码片段运行并显示谁是当前的线程。让我们将CPU时间的最后10毫秒记在它的帐上。因此,如果一个线程开始运行,并在持续运行8毫秒后完成,接着,第二个线程开始运行并持续了2毫秒,这时,时钟激发,请猜一猜这整整10毫秒的时钟周期到底记在了哪个线程的帐上?答案是第二个线程。因此,NT中存在一些固有的不准确性,而NT恰是以这种方式进行计时,实际情况也如是,大多数32位操作系统中都存在一个基于间隔的计时机制。请记住这一点,因为,有时当你观察线程所耗用的CPU总和时,会出现尽管该线程或许看上去已运行过数十万次,但其CPU时间占用量却可能是零或非常短暂的现象,那么,上述解释便是原因所在。上述也就是我们在任务管理器的进程选项卡中所能看到的基本信息列

 

 

Linux 上实现双向进程间通信管道

本文阐述了一个使用 socketpair 系统调用在 Linux 上实现双向进程通讯管道的方法,并提供了一个实现。

问题和常见方法
Linux 提供了 popen 和 pclose 函数(1) ,用于创建和关闭管道与另外一个进程进行通信。其接口如下:

FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);

遗憾的是,popen 创建的管道只能是单向的 — mode 只能是 “r” 或 “w” 而不能是某种组合–用户只能选择要么往里写,要么从中读,而不能同时在一个管道中进行读写。实际应用中,经常会有同时进行读写的要求,比如,我们可能希望把文本数据送往 sort 工具排序后再取回结果。此时 popen 就无法用上了。我们需要寻找其它的解决方案。

有一种解决方案是使用 pipe 函数(2)创建两个单向管道。没有错误检测的代码示意如下:

int pipe_in[2], pipe_out[2];
pid_t pid;
pipe(&pipe_in);     // 创建父进程中用于读取数据的管道
pipe(&pipe_out);    // 创建父进程中用于写入数据的管道
if ( (pid = fork()) == 0) {     // 子进程
close(pipe_in[0]);   // 关闭父进程的读管道的子进程读端
close(pipe_out[1]); // 关闭父进程的写管道的子进程写端
dup2(pipe_in[1], STDOUT_FILENO); // 复制父进程的读管道到子进程的标准输出
dup2(pipe_out[0], STDIN_FILENO);   // 复制父进程的写管道到子进程的标准输入
close(pipe_in[1]);   // 关闭已复制的读管道
close(pipe_out[0]); // 关闭已复制的写管道
/* 使用exec执行命令 */
} else {   // 父进程
close(pipe_in[1]);   // 关闭读管道的写端
close(pipe_out[0]); // 关闭写管道的读端
/* 现在可向pipe_out[1]中写数据,并从pipe_in[0]中读结果 */
close(pipe_out[1]); // 关闭写管道
/* 读取pipe_in[0]中的剩余数据 */
close(pipe_in[0]);   // 关闭读管道
/* 使用wait系列函数等待子进程退出并取得退出代码 */
}

当然,这样的代码的可读性(特别是加上错误处理代码之后)比较差,也不容易封装成类似于 popen/pclose 的函数,方便高层代码使用。究其原因,是 pipe 函数返回的一对文件描述符只能从第一个中读、第二个中写(至少对于 Linux 是如此)。为了同时读写,就只能采取这么累赘的两个 pipe 调用、两个文件描述符的形式了。

一个更好的方案
使用pipe就只能如此了。不过,Linux 实现了一个源自 BSD 的 socketpair 调用 (3),可以实现上述在同一个文件描述符中进行读写的功能(该调用目前也是 POSIX 规范的一部分(4) )。该系统调用能创建一对已连接的(UNIX 族)无名 socket。在 Linux 中,完全可以把这一对 socket 当成 pipe 返回的文件描述符一样使用,唯一的区别就是这一对文件描述符中的任何一个都可读和可写。

这似乎可以是一个用来实现进程间通信管道的好方法。不过,要注意的是,为了解决我前面的提出的使用 sort 的应用问题,我们需要关闭子进程的标准输入通知子进程数据已经发送完毕,而后从子进程的标准输出中读取数据直到遇到 EOF。使用两个单向管道的话每个管道可以单独关闭,因而不存在任何问题;而在使用双向管道时,如果不关闭管道就无法通知对端数据已经发送完毕,但关闭了管道又无法从中读取结果数据。——这一问题不解决的话,使用 socketpair 的设想就变得毫无意义。

令人高兴的是,shutdown 调用 (5) 可解决此问题。毕竟 socketpair 产生的文件描述符是一对 socket,socket 上的标准操作都可以使用,其中也包括 shutdown。——利用 shutdown,可以实现一个半关闭操作,通知对端本进程不再发送数据,同时仍可以利用该文件描述符接收来自对端的数据。没有错误检测的代码示意如下:

int fd[2];
pid_t pid;
socketpair(AF_UNIX, SOCKET_STREAM, 0, fd);    // 创建管道
if ( (pid = fork()) == 0) {     // 子进程
close(fd[0]);   // 关闭管道的父进程端
dup2(fd[1], STDOUT_FILENO);  // 复制管道的子进程端到标准输出
dup2(fd[1], STDIN_FILENO);     // 复制管道的子进程端到标准输入
close(fd[1]);   // 关闭已复制的读管道
/* 使用exec执行命令 */
} else {   // 父进程
close(fd[1]);   // 关闭管道的子进程端
/* 现在可在fd[0]中读写数据 */
shutdown(fd[0], SHUT_WR);       // 通知对端数据发送完毕
/* 读取剩余数据 */
close(fd[0]);   // 关闭管道
/* 使用wait系列函数等待子进程退出并取得退出代码 */
}

很清楚,这比使用两个单向管道的方案要简洁不少。我将在此基础上作进一步的封装和改进。

封装和实现
直接使用上面的方法,无论怎么看,至少也是丑陋和不方便的。程序的维护者想看到的是程序的逻辑,而不是完成一件任务的各种各样的繁琐细节。我们需要一个好的封装。

封装可以使用 C 或者 C++。此处,我按照 UNIX 的传统,提供一个类似于 POSIX 标准中 popen/pclose 函数调用的 C 封装,以保证最大程度的可用性。接口如下:

FILE *dpopen(const char *command);
int dpclose(FILE *stream);
int dphalfclose(FILE *stream);

关于接口,以下几点需要注意一下:
- 与 pipe 函数类似,dpopen 返回的是文件结构的指针,而不是文件描述符。这意味着,我们可以直接使用 fprintf 之类的函数,文件缓冲区会缓存写入管道的数据(除非使用 setbuf 函数关闭文件缓冲区),要保证数据确实写入到管道中需要使用 fflush 函数。
- 由于 dpopen 返回的是可读写的管道,所以 popen 的第二个表示读/写的参数不再需要。
- 在双向管道中我们需要通知对端写数据已经结束,此项操作由dphalfclose函数来完成。

具体的实现请直接查看程序源代码,其中有详细的注释和 doxygen 文档注释(6) 。我只略作几点说明:
- 本实现使用了一个链表来记录所有 dpopen 打开的文件指针和子进程 ID 的对应关系,因此,在同时用 dpopen 打开的管道的多的时候,dpclose(需要搜索链表)的速度会稍慢一点。我认为在通常使用过程中这不会产生什么问题。如果在某些特殊情况下这会是一个问题的话,可考虑更改 dpopen 的返回值类型和 dpclose 的传入参数类型(不太方便使用,但实现简单),或者使用哈希表/平衡树来代替目前使用的链表以加速查找(接口不变,但实现较复杂)。
- 当编译时在 gcc 中使用了 “-pthread” 命令行参数时,本实现会启用 POSIX 线程支持,使用互斥量保护对链表的访问。因此本实现可以安全地用于 POSIX 多线程环境之中。
- 与 popen 类似(7) ,dpopen 会在 fork 产生的子进程中关闭以前用 dpopen 打开的管道。
- 如果传给 dpclose 的参数不是以前用 dpopen 返回的非 NULL 值,当前实现除返回 -1 表示错误外,还会把 errno 设为 EBADF。对于 pclose 而言,这种情况在 POSIX 规范中被视为不确定(unspecified)行为 (8)。
- 实现中没有使用任何平台相关特性,以方便移植到其它 POSIX 平台上。

下面的代码展示了一个简单例子,将多行文本送到 sort 中,然后取回结果、显示出来:

#include
#include
#include “dpopen.h”

#define MAXLINE 80

int main()
{
char    line[MAXLINE];
FILE    *fp;
fp = dpopen(“sort”);
if (fp == NULL) {
perror(“dpopen error”);
exit(1);
}
fprintf(fp, “orange\n”);
fprintf(fp, “apple\n”);
fprintf(fp, “pear\n”);
if (dphalfclose(fp) < 0) {
perror(“dphalfclose error”);
exit(1);
}
for (;;) {
if (fgets(line, MAXLINE, fp) == NULL)
break;
fputs(line, stdout);
}
dpclose(fp);
return 0;
}

输出结果为:

apple
orange
pear

总结
本文阐述了一个使用 socketpair 系统调用在 Linux 上实现双向进程通讯管道的方法,并提供了一个实现。该实现提供的接口与 POSIX 规范中的 popen/pclose 函数较为接近,因而非常易于使用。该实现没有使用平台相关的特性,因而可以不加修改或只进行少量修改即可移植到支持 socketpair 调用的 POSIX 系统中去。