Chinaunix首页 | 论坛 | 博客
  • 博客访问: 101957
  • 博文数量: 34
  • 博客积分: 2500
  • 博客等级: 少校
  • 技术积分: 307
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-17 12:43
文章分类

全部博文(34)

文章存档

2011年(1)

2009年(5)

2008年(28)

我的朋友

分类: LINUX

2009-01-12 20:02:30

所谓IT,离不开不同信息数据的交换。同一操作系统中运行的不同程序之间,不同操作系统中的程序之间,甚至是不同体系架构的计算机系统之间,都会出现交换数据信息即通信的需求。现代计算机系统中的通信,归根结底是由操作系统的进程来进行的(大型的自动化系统中的每一个在运行任务的智能节点通常抽象为一个独立的进程)。上列出主要的进程间通信技术包括了:匿名管道、命名管道(fifo)、公共对象请求代理体系结构CORBAD-Bus、分布式计算环境DCE、可扩展标记语言XML、开放式网络计算技术与远程过程调用ONC RPC(即Sun ONC & RPC)、套接字Sockets等等。可见进程间通信技术事实上是一个相当广泛的概念。

本章主要讲述匿名管道、fifoXSI IPC这三种经典的UNIX系统的进程间通信技术及其应用。

1、管道

管道技术在UNIX的语境中有时候指shell中的,有时候指进程间通信程序设计中的技术,有时是匿名管道和fifo(7)的统称。本书特指匿名管道。

匿名管道技术的一个经典应用为shell中的管道线,它实现将前一个程序的标准输出成为后一个程序的标准输入。

管道技术使得UNIX系统只需要提供基本的工具零部件,用户用管道的把所需的工具组合在一起就可以实现出许多新的应用,而不用专门重新发明轮子。管道的发明对影响深远,如“Do One Thing And Do It Well”、“Keep It Simple, Stupid”、“Small Is Beautiful”、“Less Is More”等。ESR,管道技术的发明人Doug Mcllroy是在UNIX的作者Ken ThompsonDennis Ritchie之后,对早期UNIX影响最重要的人。

下面的这个命令来自网上,它通过history(1)(显示命令历史记录的shell内置工具)、awk(1)(格式化文本和正则表达式过滤工具)、sort(1)(对输入进行排序的工具)、uniq(1)(统计某种特征的输入重复次数的工具)、head(1)(显示输入的前面部分的工具)这几个工具用管道线组合起来,用于显示你在shell中最常用的前10个命令及其使用次数:

  1. history | awk '{print $2}' | awk 'BEGIN {FS="|"} {print $1}'| sort | uniq -c | sort -rn | head -10

利用netcat(1)工具,就可以把管道应用到网络上。下面的例子使用netcat将主机2的数据压缩后发送到主机1再解压缩:

主机1,监听端口12345,将网络上发送过来的数据解包到指定目录:

  1. host1 $ nc -l -p 12345 | tar zxvf - -C /home/jamnix/datatorecv/

主机2,将指定的目录(或文件)打包并压缩,通过netcat发送给主机1

  1. host2 $ tar zcvf - /home/mjxian/datatosend/ | nc host1 12345

可以用下面的函数在程序中创建一个匿名管道:

  1. #include 
  2. int pipe(int filedes[2]);

该函数用数组参数创建并打开了两个匿名(即不能通过文件名引用)的管道文件。描述符filedes[0]作为输入端,用于读取管道传来的数据,对它的write调用将失败;而filedes[1]作为输出端,用于向管道写数据,对它的read调用将失败。写入fildes[1]的数据可以从fildes[0]中读出。

进程在fork之前创建匿名管道,由于子进程继承文件描述符,就可以使用这个管道和父进程及其它兄弟进程进行通信。


应注意的几点:

  • 从一个输出端已经关闭的管道读数据时,所有数据读取完毕后read将返回0,表示已经读取完毕;而往一个输入端已经关闭的管道写数据时,将产生信号SIGPIPE,不阻塞此信号的话,write(2)调用将返回-1并设置errnoEPIPE

  • 多个进程同时并发地往一个管道写数据时,如果某个进程写入的字节PIPE_BUF时,管道数据将穿插在一起。要避免这个问题,应采取有效的同步措施;

  • 可以利用dup2(2)重定向标准输入和标准输出到管道;

管道技术的主要局限:

  • 可移植的管道是半双工的,全双工管道不能保证移植性(所以pipe(2)要使用两个文件描述符);

  • 由于匿名管道使用文件描述符实现,故对进程的属性有限制,只在父进程及其各子进程之间使用;

2popenpclose

popen(3)创建一个子进程,用于执行指定的shell命令。和system(3)不同的是,可以将此子进程的标准输入或标准输出为管道,该管道的另一端为调用进程中返回的管道文件流指针所引用:

  1. #include 
  2. FILE *popen(const char *cmdstring, const char *type);

type"r"时,子进程所执行命令的标准输出为管道的输入端,该管道的输出端为popen的返回值;type"w"时,子进程执行的命令的标准输入为管道的输出端,该管道的输入端为popen的返回值;


pclose(3)则用于关闭popen打开的该指针:

  1. #include 
  2. int pclose(FILE *fp);

该函数返回popen执行shell命令的终止状态($?)。

注意:为了防止别有用心的用户利用,使用了popen(3)函数的程序应该防止SetUID或者SetGID可能带来的破坏。可通过执行类似以下流程预防:

  1. oldeuid = geteuid();
  2. setuid(getuid());
  3. /* do your popen(3) and other SetUID tasks */
  4. ...
  5. /* task done */
  6. setuid(oldeuid);

popen可应用于构造简单的过滤器程序;

3、协同进程的概念

如果一个进程的标准输入和标准输出都在另外一个进程的控制之下,则称该进程为另一个进程(通常是其父进程)的协同进程。ksh(1)提供了关键字“|&实现协作进程;

由于协同进程是使用管道来实现的,而管道只有写端输入EOF时才会关闭读端。所以协同进程必须使用行缓冲或者无缓冲的的I/O,否则除非使用了fflush(3),否则输入时协同进程不会工作。

4fifo

fifo(7)指的是命名管道,它和匿名管道都属于管道文件,区别是能否存在于文件系统中。在文件系统中创建一个fifo文件的函数为:

  1. #include 
  2. int mkfifo(const char *pathname, mode_t mode);

以后就可以使用标准的I/O函数访问它。也可以通过mkfifo(1)命令创建一个fifo

如果当前任务使用阻塞的方式读一个fifo文件,任务将阻塞到有另外一个任务往此fifo中写数据。如果没有进程在读一个fifo文件,则对其写入数据将产生信号SIGPIPE

如果fifo文件的最后一个写进程关闭了它,则读进程将读到一个EOF


利用fifo可以实现简单的——客户端使用一个公共的fifo向服务器进程发送请求,而服务器通过hash到客户进程PID的专用fifo发送响应;

使用公共fifo要注意并发的问题;还要注意写端被所有的客户进程关闭后,服务器进程读到EOF情况下的妥善处理;

专用fifo要注意捕捉由于客户关闭fifo的输入端而产生的SIGPIPE信号,并回收已终止的客户进程的fifo

5XSI IPC

XSI IPCSystem V IPC机制为基础制定。它包括三种类型:消息队列、信号量及共享内存(书中译为“共享存储”,我这里采用更要广泛一些的译名)。它们的特点是不存在于文件系统命名空间,即不存在于文件系统,不能用访问文件的I/O方法去访问,而只能使用专门设计的系统调用。APUE2TAOUP这两本书都认为XSI IPC是历史遗留功能,新程序最好使用其它的进程间通信机制(例如管道或者套接字)来取代它们。

  1. XSI IPC对象的标识符与键

标识符使用int类型,它唯一标识了系统运行时的一个IPC对象。标识符和文件描述符有几个地方不一样:新的IPC对象标识符值总为上一次的标识符值加1,直到溢出翻转;IPC对象标识符在整个系统中是唯一的,而文件描述符只能被进程及其子进程使用。

任何进程都可以通过键(key)获得一个系统中的IPC对象标识符。有3种方法可以得到一个键;

  1. IPC_PRIVATE为键值创建一个新的IPC对象。返回的标识符可以存放在文件系统中供其它进程获得。缺点是需要读写文件;

  2. 在公用的头文件中指定一个键,通过此键创建IPC对象。缺点是可能导致重复(此时相关函数将返回出错)。

  3. 将指定的路径和id通过ftok(3)转换为一个键值,通过方法B使用此键。

  1. #include 
  2. key_t ftok(const char *path, int id);

其中,path必须为真实存在的路径,id只使用其低8位;但此种方法并不能完全避免产生相同的键。

此后可以使用IPC对象的get函数(在下面说明这些函数)创建或访问指定的IPC对象。这些函数共有的规则是:

  • key参数为IPC_PRIVATE的话表示创建一个新的IPC对象;

  • key没有被现有的IPC对象使用,并在参数flag中指定IPC_CREAT时,也创建一个新的IPC对象;判定key是否已经被使用,可以在flag中指定IPC_EXCL

  • 否则,get函数则用于通过指定的key获得对应IPC对象的标识符;

  1. 访问模式

消息队列和共享内存包括“读”和“写”两种访问模式,而信号量为“读”和“更改”两种模式。

  1. IPC对象的主要优点和缺点

  • 需要自己设计引用计数等释放对象的规则;

  • 不属于文件范畴,不能使用文件系统的open(2)read(2)write(2)I/O函数,故也不能利用已有的高级I/O机制并行地访问多个IPC对象;

  1. 消息队列

消息队列是一个链表,新建或打开一个消息队列的函数为

  1. #include 
  2. int msgget(key_t key, int flag);

管理一个消息的函数为:

  1. #include 
  2. int msgctl(int msqid, int cmd, struct mdqid_ds *buf);

注意有关消息队列的函数中给的参数有时是msq,有时是msg,前者是消息队列,后者指单个消息。

参数cmd包括:

IPC_STAT 将指定的队列msqid的属性结构msqid_ds存入buf

IPC_SET buf指定的值,修改msqiduidgidmodeqbyte4个属性。此操作需要相应权限,特别是qbyte的修改需要root权限;

IPC_RMID 删除队列msqid,立即生效。此操作需要相应权限。


往队列中添加一个消息的函数为:

  1. #include 
  2. int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);

其中消息ptr头部必须是一个long类型的消息类型,供msgrcv(2)函数检别。

flag可以设置为IPC_NOWAIT,否则函数在队列已经满的时候将被阻塞,直到有信号被递送或者队列腾出空间;


从队列中取走一个消息(该消息将直接出队)的函数为

  1. #include 
  2. int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);

如果type0,则函数取队列中最早的消息;为正整数,则取匹配消息头部的最早的消息;为负整数,则取头部所标识类型小于type绝对值的第一个消息;

flag可以为IPC_NOWAIT(不使用的话,则阻塞到队列非空或者被信号中断)或者MSG_NOERROR(不使用的话,ptr指向的空间长度nbytes不够装入匹配type的消息时将报错)等;

  1. 信号量

信号量的实质是一个同步机制,实际实现为一个引用计数器。

信号量(Semaphore)和信号(Signal)在概念上的区别:信号量(有时也称为信号灯)展现的信号带有规则性,例如交通灯就是一种Semaphore,它规定了红灯停、绿灯行等;而单纯的Signal只展现一种状态,怎么处理这种状态多数情况下是用户自主的,例如手机上的网络强度信号;

而在计算机同步机制中,信号量的值用来描述允许多少个任务引用指定资源,为0时则表示不可用。

以二元信号量最为常见,它只有10两个值,初始值为1

新建或打开一个信号量集的函数为

  1. #include 
  2. int semget(key_t key, int nsems, int flag);

nsemskey所关联的信号量集中的信号数;如果调用该函数是为了引用一个现存的信号量集,则nsems应设为0


管理一个信号量集的函数为:

  1. #include 
  2. int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
semid为指定的信号量集;semnum为集合中指定的信号量,取值范围为0~总数–1cmd为指定的操作;arg为指定的数据源,它是一个联合体类型,定义为:
  1. union semun{
  2.     int val;               /* cmd为SETVAL时作为数据源 */
  3.     struct semid_ds *buf;  /* cmd为IPC_STAT和IPC_SET时的数据源 */
  4.     unsigned short *array; /* cmd为GETALL和SETALL时的数据源 */
  5. }

cmd的取值包括:

IPC_STAT 取信号量集的属性结构semid_dsarg.buf中;

IPC_SET arg.buf中的数据修改信号量集的属性;

IPC_RMID 删除信号量集semid,立即生效;

SETVAL arg.val的值修改信号量的semval值;

GETALL semid中当前所有信号量的值保存到arg.array指向的数组;

SETALL arg.array所指向数组的数据来更新semid中的所有信号量;

GETXXX 有多种,用于使函数返回信号量的属性;


信号量的属性结构没有名字,只作描述用途而不存在实例对象,一般包括

  1. struct {
  2.     unsigned short semval;  /* 信号量当前值 */
  3.     pid_t sempid;           /* 上次访问该信号量的进程PID */
  4.     unsigned short semncnt; /* 等待该信号量释放资源的进程数 */
  5.     unsigned short semzcnt; /* 等待信号量不可用(即semval为0)的进程数 */
  6.     ...
  7. }

信号量一个不靠谱的地方是创建semget和初始化semctl是分开的,需要自行设计这种非原子的操作步骤可能带来并发问题;


信号量通过函数semop(2)来使用,该函数是以原子操作实现的:

  1. #include 
  2. int semop(int semid, struct sembuf semoparray[], size_t nops);

它使用一组sembuf对象(长度为nops)对semid集合中指定的信号量进行管理。sembuf定义为:

  1. struct sembuf{
  2.     unsigned short sem_num; /* 指定集合中的信号量,取值0 ~ nsems-1 */
  3.     short sem_op;           /* 指定操作数 */
  4.     short sem_flg;          /* 包括IPC_NOWAIT和SEM_UNDO*/
  5. }

该函数将使信号量的值semval更新为semval + sem_op

如果没有设置IPC_NOWAIT标志,且更新后semval为负数。则进程将阻塞(同时信号量的semncnt将增加1)到该信号量值被其它进程修改重新变为非负,或者到信号量被删除,或者被阻塞的进程捕捉了信号。解除阻塞时同时semncnt也相应减1

特别地,如果没有设置IPC_NOWAIT,而sem_op0,则进程将阻塞(同时信号量的semzcnt增加1)到信号量变为0,或者到信号量被删除,或者被阻塞的进程捕捉了信号。解除阻塞时同时semzcnt也相应减1


信号量和记录锁等其它同步机制相比的较明显的优势主要在:时间性能较好、可以同时锁多个资源。弱点包括:不提供原子地创建和赋初值的API、生存期独立于进程且不提供引用计数。故需要使用时,需要自行进行更多复杂的设计。

  1. 共享内存

共享内存将公共的内存单元映射到进程的地址空间(有点类似mmap(2)),使得几个进程之间可以不受限制的同时访问同一个内存区域。它是理论上最快的IPC方式。但可能需要使用适用的同步机制(一般用信号量)来处理并发。

新建或打开一个共享内存对象:

  1. #include 
  2. int shmget(key_t key, size_t size, int flag);

size应取PAGESIZE的整数倍,用作打开现存的共享内存对象时,size应设为0


管理共享内存对象:

  1. #include 
  2. int shmctl(int shmid, int cmd, struct shmid_ds *buf);

cmd包括IPC_STATIPC_SETIPC_RMIDSHM_LOCKSHM_UNLOCK(后面两个非SUS标准,仅在Linux等系统下提供)。

注意:执行IPC_RMID后,shmid立即不可用,但已经连接到进程的地址空间依然可以正常访问。


连接共享内存到进程地址空间(一般为堆栈):

  1. #include 
  2. void *shmat(int shmid, const void *addr, int flag);
addr指定进程地址空间的首址,但一般应取NULL,而让系统自行选择地址。函数返回实际连接到的地址空间的首址。出错时返回-1(而不是NULL)。

解除连接的函数为:

  1. #include 
  2. int shmdt(void *addr);


也可以通过其它技术实现内存空间共享:

使用mmap(2),设置为MAP_SHARED,参数fd引用的文件是/dev/zero。可以以这样的方式与子进程共享这块映射到的空间;fd还可以设置为-1,并设置MAP_ANON,实现匿名存储映射。

也可以干脆直接使用线程机制;

6、以上几种进程间通信机制应用在C/S模型中的比较

  • 匿名管道

父进程可以通过fork(2)传递管道文件描述符给子进程。进程间通过管道传输数据。模型简单。主要问题是有进程关系的限制。

  • fifo

典型的模型是:客户进程通过公用fifo向服务进程发请求,服务进程通过专用fifo向客户进程应答。该模型在设计上比较清晰,需要注意的问题有:处理管道异常信号、清理已失效的fifo

  • XSI IPC的消息队列

服务器和客户进程只需要一个消息队列就可以实现通信,通过不同的消息类型字段区分客户进程。但这种方式很容易让不遵循规则的恶意进程随意读取非授权的消息,而需要专门设计安全机制,例如在消息中定义相关的授权协议;

也可以实现为类似上述的fifo模型,服务器进程用公用队列,客户进程用专用队列。可以通过msgid_dsmsg_lspid(最新一次发送消息到队列的进程PID)成员获得PID,但没有一种可移植的方法可以通过PID得到EUID。且这种方式容易浪费资源,同时服务器需要专门实现多路队列的读取。


消息队列比较突出的一个问题是:由于任意进程只要拿到标识符(而无需其它授权)就可以读取消息(使得消息出队),需要针对此专门设计安全措施(见习题15.11)。

  • XSI IPC的共享内存

共享内存结合同步方法,也可以实现上述的消息队列的客户机/服务器模型;

7APUE2对本章所述的几种进程间通信机制的建议

  • 掌握匿名管道和fifo技术,因为它们清晰简单;

  • 尽量不使用消息队列和信号量,而以全双工管道和记录锁代替之;

  • 可以用mmap(2)代替XSI IPC的共享内存;

另外,ESRTAOUP中指出,System V IPC,即XSI IPC,用于定义短小二进制协议的功能多数已经被套接字机制取代。

阅读(1023) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~