1.10 system函数,pclose函数,waitpid函数的返回值是什么?
==========================================================
'system()’,'pclose()’或者'waitpid()’的返回值不象是我进程的退出值(exit
value)(译者注:退出值指调用exit() 或_exit()时给的参数)... 或者退出值左移了8
位...这是怎么搞的?
手册页是对的,你也是对的! 如果查阅手册页的'waitpid()’你会发现进程的返回
值被编码了。正常情况下,进程的返回值在高16位,而余下的位用来作其它事。
如果你希望可移植,你就不能凭借这个,而建议是你该使用提供的宏。这些宏总
是在'wait()’或'wstat’的文档中说明了。
为了不同目的定义的宏(在'<sys/wait.h>’)包括(stat是'waitpid()’返回的值):
`WIFEXITED(stat)'
如果子进程正常退出则返回非0
`WEXITSTATUS(stat)'
子进程返回的退出码
`WIFSIGNALED(stat)'
如果子进程由与信号而 终止则返回非0
`WTERMSIG(stat)'
终止子进程的信号代码
`WIFSTOPPED(stat)'
如果子进程暂停(stopped)则返回非0
`WSTOPSIG(stat)'
使子进程暂停的信号代码
`WIFCONTINUED(stat)'
如果状态是表示子进程继续执行则返回非0
`WCOREDUMP(stat)'
如果'WIFSIGNALED(stat)’为非0,而如果这个进程产生一个内存映射文件
(core dump)则返回非0
1.11 我怎样找出一个进程的存储器使用情况?
=========================================
如果提供的话,参看'getrusage()’手册页
1.12 为什么进程的大小不缩减?
=============================
当你使用'free()’函数释放内存给堆时,几乎所有的系统都*不*减少你程序的
对内存的使用。被'free()’释放的内存仍然属于进程地址空间的一部份,并将
被将来的'malloc()’请求所重复使用。
如果你真的需要释放内存给系统,参看使用'
mmap()’分配私有匿名内存映射
(private anonymous mappings)。当这些内存映射被取消映射时,内存真的将其释放给
系统。某些'malloc()’的实现方法(比如在GNU C库中)在允许时自动使用'mmap()’
实施大容量分配;这些内存块(blocks)随着'free()’被释放回系统。
当然,如果你的程序的大小增加而你认为它不应该这样,你可能有一个'内存泄
露’('memory leak’)- 即在你的的程序中有缺陷(bug)导致未用的内存没释放。
1.13 我怎样改变我程序的名字(即“ps”看到的名字)?
=================================================
在BSD风格的系统中,'ps’程序实际上审视运行进程的地址空间从而找到当前
的'argv[]’,并显示它。这使得程序可以通过简单的修改'argv[]’以改变它的
名字。
在SysV风格的系统中,命令的名字和参数的一般头80字节是存放在进程的u-区
(
u-area), 所以不能被直接修改。可能有一个系统调用用来修改它(不象是这样),
但是其它的话,只有一个方法就是实施一个'exec()’,或者些内核内存(危险,
而且只有root才有可能)。
一些系统
(值得注意的是Solaris)可以有'ps’的两种不同版本,一种是在
'/usr/bin/ps’拥有SysV的行为,而另一种在'/usr/ucb/ps’拥有BSD的行为。
在
这些系统中,如果你改变'argv[]’,那么BSD版的'ps’将反映这个变化,而
SysV
版将不会。
检查你的系统是否有一个函数'setproctitle()’。
1.14 我怎样找到进程的相应可执行文件?
=====================================
这个问题可以作为'常见未回答问题’
('Frequently Unanswered Questions’)的一
个好候选,因为事实上提出这个问题经常意味着程序的设计有缺陷。
:)
你能作的'最佳猜测’('best guess’)是通过审视'argv[0]’的值而获得。如果
它包括一个'/’,那么它可能是可执行程序的绝对或相对(对于在程序开始时的
当前目录而言)路径。如果不包括,那么你可以仿效shell对于'PATH’变量的查
询来查找这个程序。但是,不能保证成功,因为有可能执行程序时'argv[0]’是
一些任意值,也不排除这个可执行文件在执行后可能已经被更名或删除的情况。
如果所有你想做的只是能打印一个和错误消息一起出现的合适的名字,那么最好
的方法在'main()’函数中将'argv[0]’的值保存在全局变量中以供整个程序使
用。虽然没有保证说'argv[0]’的值总是有意义,但在大多数情况下它是最好的
选择。
人们询问这个问题的最普通原因是意图定位他们程序的配置文件。这被认为是
不好的形式;包含可执行文件的目录应当*只*包含可执行文件,而且基于管理的
要求经常试图将配置文件放置在和可执行文件不同的文件系统。
试图做这个的一个比较不普通但更正规的理由是允许程序调用'exec()’执行它
自己;这是一种用来完全重新初始化进程(比如被用于一些'sendmail’的版本)的
办法(比如当一个守护程序捕获一个'SIGHUP’信号)。
1.14.1 So where do I put my configuration files then?
-----------------------------------------------------
1.14.1 那么,我把配置文件放在哪里里呢?
为配置文件安排正确的目录总是取决于你使用的
Unix系统的特点;
'/var/opt/PACKAGE’,'/usr/local/lib’,'/usr/local/etc’,或者任何其它一
些可能的地方。用户自定义的配置文件通常是在'$HOME’下的以“.”开始的隐藏文件(
比如'$HOME/.exrc’)。
从一个在不同系统上都能使用的软件包
(package)的角度看,它通常意味着任何站
点范围(sitewide)的配置文件的位置有个已设定的缺省值,可能情况是使用一个在
配置脚本程序里的'--prefix’选项(Autoconf 脚本程序集做这个工作)。
你会希望允
许这个缺省值在程序执行时被一个环境变量重载。(如果你没使用配置脚本程序,
那么在编译时,将这个位置缺省值作为'-D’选项放入项目文件(Makefile),或者
将其放入一个'config.h’头文件,或做其它类似的工作)
--
用户自定义配置需要放置于一个在'$HOME’下的文件名“.”打头的文件,或者
在需要多个配置文件时,建立文件名“.”打头的子目录。(在列目录时,文件名以
“.”打头的文件或目录缺省情况下被忽略。)避免在'$HOME’建立多个文件,因
为这会造成非常杂乱的情况。当然,你也应该允许用户通过一个环境变量重载这个
位置。即使不能找到某个用户的配置文件,程序仍应当以适宜的方式执行。
1.15 为何父进程死时,我的进程未得到SIGHUP信号?
===============================================
因为本来就没有设想是这样做的。
'SIGHUP’是一个信号,它按照惯例意味着“终端线路被挂断”。它与父进程
无关,而且通常由tty驱动程序产生(并传递给前台的进程组)。
但是,作为会话管理系统
(session management system)的一部份,确切说有两种情况
下'SIGHUP’会在一个进程死时发送出:
* 当一个终端设备与一个会话相关联,而这个会话的会话首领进程死时,
'SIGHUP’被发送至这个终端设备的所有前台进程组。
* 当一个进程死去导致一个进程组变成孤儿,而且该进程组里一个或多个进程
处于*暂停*状态时,那么'SIGHUP’和'SIGCONT’被发送至这个孤儿进程
组的所有成员进程。(一个孤儿进程组是指在该进程组中没有一个成员进程的
父进程属于和该进程组相同的会话的其它进程组。)
1.16 我怎样杀死一个进程的所有派生进程?
=======================================
没有一个完全普遍的方法来做这个。虽然你可以通过处理'ps’的输出确定进
程间的相互关系,但因为它只表示系统的一瞬间的状态(snapshot)所以并不可靠。
但是,如果你启动一个子进程,而它可能生成它自己的子进程,而你意图一次杀
死整个生成的事务(job),解决方法是将最先启动的子进程置于一个新的进程组,
当你需要时杀死整个进程组。
建议为创建进程组而使用的函数是'setpgid()’。在可能情况下,使用这个函数
而不使用'setpgrp()’,因为后一个在不同系统中有所不同(在一些系统上'setgrp();’
等同于'setpgid(0,0);’,在其它系统上,'setpgrp()’和'setpgid()’相同)。
参见范例章节的事务-控制范例程序。
放置一个子进程于其自身的进程组有一些影响。特别的,除非你显式地将该进程
组放置于前台,它将被认为是一个后台事务并具有以下结果:
* 试图从终端读取的进程将被'SIGTTIN’信号暂停。
* 如果设置终端模式'tostop’,那么试图向终端写的进程将被'SIGTTOU’
信号暂停。(试图改变终端模式也导致这个结果,且不管当前'tostop’是否
设置)
* 子进程将不会收到从终端发出的键盘信号(比如'SIGINT’或'SIGQUIT’)
在很多应用程序中输入和输出总会被重定向,所以最显著的影响将是丧失键盘
信号。父进程需要安排程序起码捕获'SIGINIT’和'SIGQUIT’(可能情况下,
还有'SIGTERM’),并在需要情况下清除后台事务。
2. 一般文件操作(包括管道和套接字)
*********************************
请同时参考套接字
FAQ,在
http://www.lcg.org/sock-faq/
2.1 如何管理多个连接?
======================
“我想同时监控一个以上的文件描述符(fd)/连接(connection)/流(stream),
应该怎么办?”
使用 select() 或 poll() 函数。
注意:
select() 在BSD中被引入,而poll()是SysV STREAM流控制的产物。因此,
这里就有了平台移植上的考虑:纯粹的BSD系统可能仍然缺少poll(),而早一些
的SVR3系统中可能没有select(),尽管在SVR4中将其加入。
目前两者都是
POSIX.
1g标准,(译者注:因此在Linux上两者都存在)
select()和poll()本质上来讲做的是同一件事,只是完成的方法不一样。两者都
通过检验一组文件描述符来检测是否有特定的时间将在上面发生并在一定的时间
内等待其发生。
[重要事项:无论select()还是poll()都不对普通文件起很大效用,它们着重用
于套接口(socket)、管道(pipe)、伪终端(pty)、终端设备(tty)和其他一些字符
设备,但是这些操作都是系统相关(system-dependent)的。]
2.2.1 我如何使用select()函数?
------------------------------
select()函数的接口主要是建立在一种叫'fd_set'类型的基础上。它('fd_set')
是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此
应该用一组标准的宏定义来处理此类变量:
fd_set set;
FD_ZERO(&set); /* 将set清零 */
FD_SET(fd, &set); /* 将fd加入set */
FD_CLR(fd, &set); /* 将fd从set中清除 */
FD_ISSET(fd, &set); /* 如果fd在set中则真 */
在过去,一个fd_set通常只能包含少于等于32个文件描述符,因为fd_set其实只
用了一个int的比特矢量来实现,在大多数情况下,检查fd_set能包括任意值的
文件描述符是系统的责任,但确定你的fd_set到底能放多少有时你应该检查/修
改宏FD_SETSIZE的值。*这个值是系统相关的*,同时检查你的系统中的
select()
的man手册。有一些系统对多于1024个文件描述符的支持有问题。[译者注:
Linux
就是这样的系统!你会发现sizeof(fd_set)的结果是
128(*8 =
FD_SETSIZE=1024) 尽管很少你会遇到这种情况。]
select的基本接口十分简单:
int select(int nfds, fd_set *readset, fd_set *writeset,
fd_set *exceptset, struct timeval *timeout);
其中:
nfds : 需要检查的文件描述符个数,数值应该比是三组fd_set中最大数
更大,而不是实际文件描述符的总数。
readset: 用来检查可读性的一组文件描述符。
writeset: 用来检查可写性的一组文件描述符。
exceptset: 用来检查意外状态的文件描述符。(注:错误并不是意外状态
)
timeout: NULL指针代表无限等待,否则是指向timeval结构的指针,代表最
长等待时间。(如果其中tv_sec和tv_usec都等于0, 则文件描述符
的状态不被影响,但函数并不挂起)
函数将返回响应操作的对应操作文件描述符的总数,且三组数据均在恰当位置被
修改,只有响应操作的那一些没有修改。接着应该用FD_ISSET宏来查找返回的文
件描述符组。
这里是一个简单的测试单个文件描述符可读性的例子:
int isready(int fd)
{
int rc;
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd,&fds);
tv.tv_sec = tv.tv_usec = 0;
rc = select(fd+1, &fds, NULL, NULL, &tv);
if (rc < 0)
return -1;
return FD_ISSET(fd,&fds) ? 1 : 0;
}
当然如果我们把NULL指针作为fd_set传入的话,这就表示我们对这种操作的发生
不感兴趣,但select() 还是会等待直到其发生或者超过等待时间。
[译者注:在Linux中,timeout指的是程序在非sleep状态中度过的时间,而不是
实际上过去的时间,这就会引起和非Linux平台移植上的时间不等问题。移植问
题还包括在System V风格中select()在函数退出前会把timeout设为未定义的
NULL
状态,而在BSD中则不是这样,Linux在这点上遵从System V,因此在重复利
用timeout指针问题上也应该注意。]
2.1.2 我如何使用poll()?
------------------------
poll()
接受一个指向结构'struct pollfd'列表的指针,其中包括了你想测试的
文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前的结构在
调用后将被填写并在事件发生后返回。在SVR4(可能更早的一些版本)中的
"poll.h"
文件中包含了用于确定事件的一些宏定义。事件的等待时间精确到毫秒
(
但令人困惑的是等待时间的类型却是int),当等待时间为0时,poll()函数立即
返回,-1则使poll()一直挂起直到一个指定事件发生。下面是pollfd的结构。
struct pollfd {
int fd; /* 文件描述符
*/
short events; /* 等待的事件
*/
short revents; /* 实际发生了的事件
*/
};
于select()十分相似,当返回正值时,代表满足响应事件的文件描述符的个数,
如果返回0则代表在规定事件内没有事件发生。如发现返回为负则应该立即查看
errno
,因为这代表有错误发生。
如果没有事件发生,revents会被清空,所以你不必多此一举。
这里是一个例子:
/* 检测两个文件描述符,分别为一般数据和高优先数据。如果事件发生
则用相关描述符和优先度调用函数handler(),无时间限制等待,直到
错误发生或描述符挂起。
*/
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <stropts.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define NORMAL_DATA 1
#define HIPRI_DATA 2
int poll_two_normal(int fd1,int fd2)
{
struct pollfd poll_list[2];
int retval;
poll_list[0].fd = fd1;
poll_list[1].fd = fd2;
poll_list[0].events = POLLIN|POLLPRI;
poll_list[1].events = POLLIN|POLLPRI;
while(1)
{
retval = poll(poll_list,(unsigned long)2,-1);
/* retval 总是大于0或为-1,因为我们在阻塞中工作
*/
if(retval < 0)
{
fprintf(stderr,"poll错误
: %s\n",strerror(errno));
return -1;
}
if(((poll_list[0].revents&POLLHUP) == POLLHUP) ||
((poll_list[0].revents&POLLERR) == POLLERR) ||
((poll_list[0].revents&POLLNVAL) == POLLNVAL) ||
((poll_list[1].revents&POLLHUP) == POLLHUP) ||
((poll_list[1].revents&POLLERR) == POLLERR) ||
((poll_list[1].revents&POLLNVAL) == POLLNVAL))
return 0;
if((poll_list[0].revents&POLLIN) == POLLIN)
handle(poll_list[0].fd,NORMAL_DATA);
if((poll_list[0].revents&POLLPRI) == POLLPRI)
handle(poll_list[0].fd,HIPRI_DATA);
if((poll_list[1].revents&POLLIN) == POLLIN)
handle(poll_list[1].fd,NORMAL_DATA);
if((poll_list[1].revents&POLLPRI) == POLLPRI)
handle(poll_list[1].fd,HIPRI_DATA);
}
}
2.1.3 我是否可以同时使用SysV IPC和select()/poll()?
---------------------------------------------------
*不能。* (除非在AIX上,因为它用一个无比奇怪的方法来实现这种组合)
一般来说,同时使用
select()或poll()和SysV 消息队列会带来许多麻烦。SysV
IPC的对象并不是用文件描述符来处理的,所以它们不能被传递给select()和
poll()。这里有几种解决方法,其粗暴程度各不相同:
- 完全放弃使用SysV IPC。
:-)
- 用fork(),然后让子进程来处理SysV IPC,然后用管道或套接口和父进程
说话。父进程则使用select()。
-
同上,但让子进程用select(),然后和父亲用消息队列交流。
-
安排进程发送消息给你,在发送消息后再发送一个信号。*警告*:要做好
这个并不简单,非常容易写出会丢失消息或引起死锁的程序。
……另外还有其他方法。
2.2 我如何才能知道和对方的连接被终止?
======================================
如果你在读取一个管道、套接口、FIFO等设备时,当写入端关闭连接时,你将会
得到一个文件结束符(EOF)(read()返回零字节读取)。如果你试图向一个管道或
套接口写入,当读取方关闭连接,你将得到一个SIGPIPE的信号,它会使进程终
止除非指定处理方法。(如果你选择屏蔽或忽略信号,write()会以EPIPE错误退
出。)
2.3 什么是读取目录的最好方法?
==============================
历史上曾有过许多不同的目录读取方法,但目前你应该使用POSIX.1标准的
<dirent.h>
接口。
opendir()函数打开一个指定的目录;readdir()将目录以一种标准的格式读入;
closedir()关闭描述符。还有一些其他如rewinddir()、telldir()和seekdir()
等函数,相信不难理解。
如果你想用文件匹配符('*','?'),那么你可以使用大多数系统中都存在的
glob()
函数,或者可以查看fnmatch()函数来得到匹配的文件名,或者用ftw()来遍历整
个目录树。
2.4 我如何才能知道一个文件被另外进程打开?
==========================================
这又是一个“经常不被回答的问题”,因为一般来说你的程序不会关心文件是否
正被别人打开。如果你需要处理文件的并发操作,那你应该使用咨询性文件锁。
一般来说要做到这点很难,像fuser或lsof这样可以告诉你文件使用情况的工具
通过解析内核数据来达到目的,但这种方法十分不健康!而且你不能从你的程序
中调用它们来获取信息,因为也许当它们执行完成之后,文件的使用状况在瞬间
又发生了变化,你无法保证这些信息的正确。
2.5 我如何锁住一个文件?
========================
有三种不同的文件锁,这三种都是“咨询性”的,也就是说它们依靠程序之间的
合作,所以一个项目中的所有程序封锁政策的一致是非常重要的,当你的程序需
要和第三方软件共享文件时应该格外地小心。
有些程序利用诸如 FIlENAME.lock 的文件锁文件,然后简单地测试此类文件是否
存在。这种方法显然不太好,因为当产生文件的进程被杀后,锁文件依然存在,
这样文件也许会被永久锁住。UUCP中把产生文件的进程号PID存入文件,但这样做
仍然不保险,因为PID的利用是回收型的。
这里是三个文件锁函数:
flock();
lockf();
fcntl();
flock()是从BSD中衍生出来的,但目前在大多数UNIX系统上都能找到,在单个主
机上flock()简单有效,但它不能在NFS上工作。Perl中也有一个有点让人迷惑的
flock()
函数,但却是在perl内部实现的。
fcntl()是唯一的符合POSIX标准的文件锁实现,所以也是唯一可移植的。它也同
时是最强大的文件锁——也是最难用的。在NFS文件系统上,fcntl()请求会被递
交给叫rpc.lockd的守护进程,然后由它负责和主机端的lockd对话,和
flock()
不同,fcntl()可以实现记录层上的封锁。
lockf()只是一个简化了的fcntl()文件锁接口。
无论你使用哪一种文件锁,请一定记住在锁生效之前用sync来更新你所有的文件
输入/输出。
lock(fd);
write_to(some_function_of(fd));
flush_output_to(fd); /* 在去锁之前一定要冲洗输出 */
unlock(fd);
do_something_else; /* 也许另外一个进程会更新它 */
lock(fd);
seek(fd, somewhere); /* 因为原来的文件指针已不安全 */
do_something_with(fd);
...
一些有用的fcntl()封锁方法(为了简洁略去错误处理):
#include <fcntl.h>
#include <unistd.h>
read_lock(int fd) /* 整个文件上的一个共享的文件锁 */
{
fcntl(fd, F_SETLKW, file_lock(F_RDLCK, SEEK_SET));
}
write_lock(int fd) /* 整个文件上的一个排外文件锁 */
{
fcntl(fd, F_SETLKW, file_lock(F_WRLCK, SEEK_SET));
}
append_lock(int fd) /* 一个封锁文件结尾的锁,
其他进程可以访问现有内容 */
{
fcntl(fd, F_SETLKW, file_lock(F_WRLCK, SEEK_END));
}
前面所用的file_lock函数如下:
struct flock* file_lock(short type, short whence)
{
static struct flock ret ;
ret.l_type = type ;
ret.l_start = 0 ;
ret.l_whence = whence ;
ret.l_len = 0 ;
ret.l_pid = getpid() ;
return &ret ;
}
2.6 我如何能发现一个文件已由另外一个进程更新?
==============================================
这又几乎是一个经常不被回答的问题,因为问这个问题的人通常期待能有一个系
统级的告示来反映当前目录或文件被修改,但没有什么保证移植性的实现方法,
IRIX
有一个非标准的功能用来监测文件操作,但从未听说在其他平台上也有相类
似的功能。
一般来说,你能尽的最大努力就是用fstat()函数,通过监视文件的mtime和
ctime
你能得知文件什么时候被修改了,或者被删除/连接/改名了,听起来很复杂,所
以你应该反思一下为什么你要做这些。
2.7 请问du是怎样工作的?
========================
du
只简单地用stat()(更准确地说是用lstat()函数)遍历目录结构中的每个文件
和目录,并将它们所占用的磁盘块加在一起。
如果你想知道其中细节,总是这么一句话:“读下源代码吧,老兄!”
BSD(FreeBSD、NetBSD和OpenBSD)的源代码在这些发行的FTP网站的源码目录里,
GNU版本的源码当然可以在任何一个GNU镜像站点中找到——前提是你自己懂得如
何解包。
2.8 我如何得到一个文件的长度?
==============================
用stat()或在文件打开后用fstat()。
这两个调用会将文件信息填入一个结构中, 其中你能找到诸如文件主人、属性、
大小、最后访问时间、最后修改时间等所有关于此文件的东西。
下面的程序大体示范如何用stat()得到文件大小。
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int get_file_size(char *path,off_t *size)
{
struct stat file_stats;
if(stat(path,&file_stats))
return -1;
*size = file_stats.st_size;
return 0;
}
2.9 我如何像shell里一样扩展在文件名里的'~'?
============================================
'~'
的标准用法如下:如果单独使用或者后面跟一个'/',那么'~'就被当作当前
用户的home目录,[译者注:事实上'~'就被替换为$HOME环境变量],如果'~'后
直接跟一个用户名,则被替换的就是那个用户的home目录。如果没有合适的匹
配,则shell不会做任何改动。
请注意,有可能一些文件的确是以'~'打头的,不分青红皂白地将'~'替换会使你
的程序无法打开这些文件。一般来说,从shell通过命令行或环境变量传递入程
序的文件名不须要进行替换,因为shell已经替你做好,而程序自己生成的、用
户输入的,或从配置文件中读取的却应该进行替换。
这里是一段用标准string类的C++实现:
string expand_path(const string& path)
{
if (path.length() == 0 || path[0] != '~')
return path;
const char *pfx = NULL;
string::size_type pos = path.find_first_of('/');
if (path.length() == 1 || pos == 1)
{
pfx = getenv("HOME");
if (!pfx)
{
// 我们想替换"~/",但$HOME却没有设置
struct passwd *pw = getpwuid(getuid());
if (pw)
pfx = pw->pw_dir;
}
}
else
{
string user(path,1,(pos==string::npos) ? string::npos : pos-1);
struct passwd *pw = getpwnam(user.c_str());
if (pw)
pfx = pw->pw_dir;
}
// 如果我们不能找到能替换的选择,则将path返回
if (!pfx)
return path;
string result(pfx);
if (pos == string::npos)
return result;
if (result.length() == 0 || result[result.length()-1] != '/')
result += '/';
result += path.substr(pos+1);
return result;
}
2.10 有名管道(FIFO)能做什么?
=============================
2.10.1 什么是有名管道?
-----------------------
有名管道是一个能在互不相关进程之间传送数据的特殊文件。一个或多个进程向
内写入数据,在另一端由一个进程负责读出。有名管道是在文件系统中可见的,
也就是说ls可以直接看到。(有名管道又称FIFO,也就是先入先出。)
有名管道可以将无关的进程联系起来,而无名的普通管道一般只能将父子进程联
系起来——除非你很努力地去尝试——当然也能联系两个无关进程。有名管道是
严格单向的,尽管在一些系统中无名管道是双向的。
2.10.2 我如何建立一个有名管道?
-------------------------------
在shell下交互地建立一个有名管道,你可以用mknod或mkfifo命令。在有些系统
中,mknod产生的文件可能在/etc目录下,也就是说,可能不在你的目录下出现,
所以请查看你系统中的man手册。[译者注:在Linux下,可以看一下fifo(4)]
要在程序中建立一个有名管道:
/* 明确设置umask,因为你不知道谁会读写管道
*/
umask(0);
if (mkfifo("test_fifo", S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP))
{
perror("mkfifo");
exit(1);
}
也可以使用
mknod。[译者注:在Linux下不推荐使用mknod,因为其中有许多臭虫
在NFS下工作更要小心,能使用mkfifo就不要用mknod,因为mkfifo()是POSIX.1
标准。]
/* 明确设置umask,因为你不知道谁会读写管道 */
umask(0);
if (mknod("test_fifo",
S_IFIFO | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP,
0))
{
perror("mknod");
exit(1);
}
2.10.3 我如何使用一个有名管道?
-------------------------------
使用有名管道十分简单:你如同使用一个普通文件一样打开它,用read()和
write()
进行操作。但对管道使用open()时可能引起阻塞,下面一些常用规律可
以参考。
* 如果你同时用读写方式(O_RDWR)方式打开,则不会引起阻塞。
* 如果你用只读方式(O_RDONLY)方式打开,则open()会阻塞一直到有写方打
开管道,除非你指定了O_NONBLOCK,来保证打开成功。
* 同样以写方式(O_WRONLY)打开也会阻塞到有读方打开管道,不同的是如果
O_NONBLOCK被指定open()会以失败告终。
当对有名管道进行读写的时,注意点和操作普通管道和套接字时一样:当写入方
关闭连接时read()返回EOF,如果没有听众write()会得到一个SIGPIPE的信号,
对此信号进行屏蔽或忽略会引发一个EPIPE错误退出。
2.10.4 能否在NFS上使用有名管道?
--------------------------------
不能,在NFS协议中没有相关功能。(你可能可以在一个NFS文件系统中用有名管
道联系两个同在客户端的进程。)
2.10.5 能否让多个进程同时向有名管道内写入数据?
-----------------------------------------------
如果每次写入的数据少于PIPE_BUF的大小,那么就不会出现数据交叉的情况。但
由于对写入的多少没有限制,而read()操作会读取尽可能多的数据,因此你不能
知道数据到底是谁写入的。
PIPE_BUF的大小根据POSIX标准不能小于512,一些系统里在<limits.h>中被定
义,[译者注:Linux中不是,其值是4096。]这可以通过pathconf()或
fpathconf()
对单独管道进行咨询得到。
2.10.6 有名管道的应用
---------------------
“我如何时间服务器和多个客户端的双向交流?”
一对多的形式经常出现,只要每次客户端向服务器发出的指令小于PIPE_BUF,它
们就可以通过一个有名管道向服务器发送数据。客户端可以很容易地知道服务器
传发数据的管道名。
但问题在于,服务器不能用一个管道来和所有客户打交道。如果不止一个客户在
读一个管道,是无法确保每个客户都得到自己对应的回复。
一个办法就是每个客户在向服务器发送信息前都建立自己的读入管道,或让服务
器在得到数据后建立管道。使用客户的进程号(pid)作为管道名是一种常用的方
法。客户可以先把自己的进程号告诉服务器,然后到那个以自己进程号命名的管
道中读取回复。