您现在的位置是:首页 > 缘文分享缘文分享

Unix常见问题

2020-11-10【缘文分享】人已围观

简介缘文分享:内心如果平静,外在就不会有风波。

文章中涉及许多Unix中非常有用及细节的编程问题

1. 进程控制
***********

1.1 创建新进程:fork函数
========================

1.1.1 fork函数干什么?
----------------------

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

‘fork()’函数用于从已存在进程中创建一个新进程。新进程称为子进程,而原进程称为
父进程。你可以通过检查‘fork()’函数的返回值知道哪个是父进程,哪个是子进程。父
进程得到的返回值是子进程的进程号,而子进程则返回0。以下这个范例程序说明它的基本
功能:

pid_t pid;

switch (pid = fork())
{
case -1:
/* 这里pid为-1,fork函数失败 */
/* 一些可能的原因是 */
/* 进程数或虚拟内存用尽 */
perror("The fork failed!");
break;

case 0:
/* pid为0,子进程 */
/* 这里,我们是孩子,要做什么? */
/* ... */
/* 但是做完后, 我们需要做类似下面: */
_exit(0);

default:
/* pid大于0,为父进程得到的子进程号 */
printf("Child's pid is %d\n",pid);
}

当然,有人可以用‘if() ... else ...’语句取代‘switch()’语句,但是上面的形式是
一个有用的惯用方法。

知道子进程自父进程继承什么或未继承什么将有助于我们。下面这个名单会因为
不同Unix的实现而发生变化,所以或许准确性有了水份。请注意子进程得到的是
这些东西的 *拷贝*,不是它们本身。

由子进程自父进程继承到:

* 进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs))

* 环境(environment)

* 堆栈

* 内存

* 打开文件的描述符(注意对应的文件的位置由父子进程共享,这会引起含糊情况)

* 执行时关闭(close-on-exec) 标志 (译者注:close-on-exec标志可通过fnctl()对文件描
述符设置,POSIX.1要求所有目录流都必须在exec函数调用时关闭。更详细说明,
参见<<UNIX环境高级编程>> W. R. Stevens, 1993, 尤晋元等译(以下简称<<高级编
程>>), 3.13节和8.9节)

* 信号(signal)控制设定

* nice值 (译者注:nice值由nice函数设定,该值表示进程的优先级,数值越小,优
先级越高)

* 进程调度类别(scheduler class) (译者注:进程调度类别指进程在系统中被调度时所
属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计
算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行)

* 进程组号

* 对话期ID(Session ID) (译者注:译文取自<<高级编程>>,指:进程所属的对话期
(session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见<<高级编程>>
9.5节)

* 当前工作目录

* 根目录 (译者注:根目录不一定是“/”,它可由chroot函数改变)

* 文件方式创建屏蔽字(file mode creation mask (umask)) (译者注:译文取自<<高级编
程>>,指:创建新文件的缺省屏蔽字)

* 资源限制

* 控制终端

子进程所独有:

* 进程号

* 不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同,父进
程号可由getppid函数得到)

* 自己的文件描述符和目录流的拷贝(译者注:目录流由opendir函数创建,因其为
顺序读取,顾称“目录流”)

* 子进程不继承父进程的进程,正文(text),数据和其它锁定内存(memory locks)
(译者注:锁定内存指被锁定的虚拟内存页,锁定后,不允许内核将其在必要时
换出(page out),详细说明参见<<The GNU C Library Reference Manual>> 2.2版,
1999, 3.4.2节)

* 在tms结构中的系统时间(译者注:tms结构可由times函数获得,它保存四个数据
用于记录进程使用中央处理器(CPU:Central Processing Unit)的时间,包括:用户时
间,系统时间,用户各子进程合计时间,系统各子进程合计时间)

* 资源使用(resource utilizations)设定为0

* 阻塞信号集初始化为空集(译者注:原文此处不明确,译文根据fork函数手册页
稍做修改)

* 不继承由timer_create函数创建的计时器

* 不继承异步输入和输出

1.1.2 fork函数 与 vfork函数的区别在哪里里?
-------------------------------------------

有些系统有一个系统调用‘vfork()’,它最初被设计成‘fork()’的较少额外支出
(lower-overhead)版本。因为‘fork()’包括拷贝整个进程的地址空间,所以非常
“昂贵”,这个‘vfork()’函数因此被引入。(在3.0BSD中)(译者注:BSD:
Berkeley Software Distribution)

*但是*,自从‘vfork()’被引入,‘fork()’的实现方法得到了很大改善,最值得
注意的是“写操作时拷贝”(copy-on-write)的引入,它是通过允许父子进程可访问
相同物理内存从而伪装(fake)了对进程地址空间的真实拷贝,直到有进程改变内
存中数据时才拷贝。这个提高很大程度上抹杀了需要‘vfork()’的理由;事实上,
一大部份系统完全丧失了‘vfork()’的原始功能。但为了兼容,它们仍然提供
‘vfork()’函数调用,但它只是简单地调用‘fork()’,而不试图模拟所有‘vfork()’
的语义(semantics, 译文取自<<高级编程>>,指定义的内容和做法)。

结论是,试图使用任何‘fork()’和‘vfork()’的不同点是*很*不明智的。事实上,
可能使用‘vfork()’根本就是不明智的,除非你确切知道你想*干什么*。

两者的基本区别在于当使用‘vfork()’创建新进程时,父进程将被暂时阻塞,而
子进程则可以借用父进程的地址空间。这个奇特状态将持续直到子进程要么退
出,要么调用‘execve()’,至此父进程才继续执行。

这意味着一个由‘vfork()’创建的子进程必须小心以免出乎意料地改变父进程的
变量。特别的,子进程必须不从包含‘vfork()’调用的函数返回,而且必须不调
用‘exit()’(如果它需要退出,它需要使用‘_exit()’;事实上,对于使用正常
‘fork()’创建的子进程这也是正确的)(译者注:参见1.1.3)

1.1.3 为何在一个fork的子进程分支中使用_exit函数而不使用exit函数?
-----------------------------------------------------------------

‘exit()’与‘_exit()’有不少区别在使用‘fork()’,特别是‘vfork()’时变得很
突出。

‘exit()’与‘_exit()’的基本区别在于前一个调用实施与调用库里用户状态结构
(user-mode constructs)有关的清除工作(clean-up),而且调用用户自定义的清除程序
(译者注:自定义清除程序由atexit函数定义,可定义多次,并以倒序执行),相对
应,后一个函数只为进程实施内核清除工作。

在由‘fork()’创建的子进程分支里,正常情况下使用‘exit()’是不正确的,这是
因为使用它会导致标准输入输出(译者注:stdio: Standard Input Output)的缓冲区被
清空两次,而且临时文件被出乎意料的删除(译者注:临时文件由tmpfile函数创建
在系统临时目录下,文件名由系统随机生成)。在C++程序中情况会更糟,因为静
态目标(static objects)的析构函数(destructors)可以被错误地执行。(还有一些特殊情
况,比如守护程序,它们的*父进程*需要调用‘_exit()’而不是子进程;适用于绝
大多数情况的基本规则是,‘exit()’在每一次进入‘main’函数后只调用一次。)

在由‘vfork()’创建的子进程分支里,‘exit()’的使用将更加危险,因为它将影响
*父*进程的状态。

1.2 环境变量
============

1.2.1 如何从程序中获得/设置环境变量?
--------------------------------------
获得一个环境变量可以通过调用‘getenv()’函数完成。

#include <stdlib.h>

char *getenv(const char *name);

设置一个环境变量可以通过调用‘putenv()’函数完成。

#include <stdlib.h>

int putenv(char *string);

变量string应该遵守"name=value"的格式。已经传递给putenv函数的字符串*不*能够被
释放或变成无效,因为一个指向它的指针将由‘putenv()’保存。这意味着它必须是
在静态数据区中或是从堆(heap)分配的。如果这个环境变量被另一个‘putenv()’的
调用重新定义或删除,上述字符串可以被释放。

/* 译者增加:

因为putenv()有这样的局限,在使用中经常会导致一些错
误,GNU libc 中还包括了两个BSD风格的函数:
#include <stdlib.h>
int setenv(const char *name, const char *value, int replace);
void unsetenv(const char *name);

setenv()/unsetenv()函数可以完成所有putenv()能做的事。setenv() 可以不受指针
限制地向环境变量中添加新值,但传入参数不能为空(NULL)。当replace为0时,如
果环境变量中已经有了name项,函数什么也不做(保留原项),否则原项被覆盖。
unsetenv()是用来把name项从环境变量中删除。注意:这两个函数只存在在BSD和GNU
库中,其他如SunOS系统中不包括它们,因此将会带来一些兼容问题。我们可以用
getenv()/putenv()来实现:

int setenv(const char *name, const char *value, int replace)
{
char *envstr;

if (name == NULL || value == NULL)
return 1;
if (getenv(name) !=NULL)
{
envstr = (char *) malloc(strlen(name) + strlen(value) + 2);
sprintf (envstr, "%s=%s", name, value);
if (putenv(envstr));
return 1;
}
return 0;
}
*/

记住环境变量是被继承的;每一个进程有一个不同的环境变量表拷贝(译者注:
从core文件中我们可以看出这一点)。结果是,你不能从一个其他进程改变当前
进程的环境变量,比如shell进程。

假设你想得到环境变量‘TERM’的值,你需要使用下面的程序:

char *envvar;

envvar=getenv("TERM");

printf("The value for the environment variable TERM is ");
if(envvar)
{
printf("%s\n",envvar);
}
else
{
printf("not set.\n");
}

现在假设你想创建一个新的环境变量,变量名为‘MYVAR’,值为‘MYVAL’。
以下是你将怎样做:

static char envbuf[256];

sprintf(envbuf,"MYVAR=%s","MYVAL");

if(putenv(envbuf))
{
printf("Sorry, putenv() couldn't find the memory for %s\n",envbuf);
/* Might exit() or something here if you can't live without it */
}

1.2.2 我怎样读取整个环境变量表?
--------------------------------

如果你不知道确切你想要的环境变量的名字,那么‘getenv()’函数不是很有用。
在这种情况下,你必须更深入了解环境变量表的存储方式。

全局变量,‘char **envrion’,包含指向环境字符串指针数组的指针,每一个字
符串的形式为‘“NAME=value”’(译者注:和putenv()中的“string”的格式相同)。
这个数组以一个‘空’(NULL)指针标记结束。这里是一个打印当前环境变量列表
的小程序(类似‘printenv’)。

#include <stdio.h>

extern char **environ;

int main()
{
char **ep = environ;
char *p;
while ((p = *ep++))
printf("%s\n", p);
return 0;
}

一般情况下,‘envrion’变量作为可选的第三个参数传递给‘main()’;就是说,
上面的程序可以写成:

#include <stdio.h>

int main(int argc, char **argv, char **envp)
{
char *p;
while ((p = *envp++))
printf("%s\n", p);
return 0;
}

虽然这种方法被广泛的操纵系统所支持(译者注:包括DOS),这种方法事实上并
没有被POSIX(译者注:POSIX: Portable Operating System Interace)标准所定义。(一
般的,它也比较没用)

1.3 我怎样睡眠小于一秒?
========================

在所有Unix中都有的‘sleep()’函数只允许以秒计算的时间间隔。如果你想要更
细化,那么你需要寻找替换方法:

* 许多系统有一个‘usleep()’函数

* 你可以使用‘select()’或‘poll()’,并设置成无文件描述符并试验;一个普
遍技巧是基于其中一个函数写一个‘usleep()’函数。(参见comp.unix.questions
FAQ 的一些例子)

* 如果你的系统有itimers(很多是有的)(译者注:setitimer和getitimer是两个操作
itimers的函数,使用“man setitimer”确认你的系统支持),你可以用它们自己撺一
个‘usleep()’。(参见BSD源程序的‘usleep()’以便知道怎样做)

* 如果你有POSIX实时(realtime)支持,那会有一个‘nanosleep()’函数。

众观以上方法,‘select()’可能是移植性最好的(直截了当说,它经常比
‘usleep()’或基于itimer的方法更有效)。但是,在睡眠中捕获信号的做法会有
所不同;基于不同应用,这可以成为或不成为一个问题。

无论你选择哪条路,意识到你将受到系统计时器分辨率的限制是很重要的(一
些系统允许设置非常短的时间间隔,而其他的系统有一个分辨率,比如说10毫
秒,而且总是将所有设置时间取整到那个值)。而且,关于‘sleep()’,你设置
的延迟只是最小值(译者注:实际延迟的最小值);经过这段时间的延迟,会有
一个中间时间间隔直到你的进程重新被调度到。

1.4 我怎样得到一个更细分时间单位的alarm函数版本?
==================================================

当今Unix系统倾向于使用‘setitimer()’函数实现闹钟,它比简单的‘alarm()’函
数具有更高的分辨率和更多的选择项。一个使用者一般需要首先假设‘alarm()’
和‘setitimer(ITIMER_REAL)’可能是相同的底层计时器,而且假设同时使用两
种方法会造成混乱。

Itimers可被用于实现一次性或重复信号;而且一般有3种不同的计时器可以用:

`ITIMER_REAL'
计数真实(挂钟)时间,然后发送‘SIGALRM’信号

`ITIMER_VIRTUAL'
计数进程虚拟(用户中央处理器)时间,然后发送‘SIGVTALRM’信号

`ITIMER_PROF'
计数用户和系统中央处理器时间,然后发送‘SIGPROF’信号;它供解释器
用来进行梗概处理(profiling)

然而itimers不是许多标准的一部份,尽管它自从4.2BSD就被提供。POSIX实时标
准的扩充定义了类似但不同的函数。

1.5 父子进程如何通信?
======================

一对父子进程可以通过正常的进程间通信的办法(管道,套接字,消息队列,共
享内存)进行通信,但也可以通过利用它们作为父子进程的相互关系而具有的一
些特殊方法。

一个最显然的方法是父进程可以得到子进程的退出状态。

因为子进程从它的父进程继承文件描述符,所以父进程可以打开一个管道的两端,
然后fork,然后父进程关闭管道这一端,子进程关闭管道另一端。这正是你从你的
进程调用‘popen()’函数运行另一个程序所发生的情况,也就是说你可以向
‘popen()’返回的文件描述符进行写操作而子进程将其当作自己的标准输入,或
者你可以读取这个文件描述符来看子进程向标准输出写了什么。(‘popen()’函数
的mode参数定义你的意图(译者注:mode=“r”为读,mode=“w”为写);如果你
想读写都做,那么你可以并不困难地用管道自己做到)

而且,子进程继承由父进程用mmap函数映射的匿名共享内存段(或者通过映射特
殊文件‘/dev/zero’);这些共享内存段不能从无关的进程访问。

1.6 我怎样去除僵死进程?
========================

1.6.1 何为僵死进程?
--------------------

当一个程序创建的子进程比父进程提前结束,内核仍然保存一些它的信息以便父
进程会需要它 - 比如,父进程可能需要检查子进程的退出状态。为了得到这些信
息,父进程调用‘wait()’;当这个调用发生,内核可以丢弃这些信息。

在子进程终止后到父进程调用‘wait()’前的时间里,子进程被称为‘僵死进程’
(‘zombie’)。(如果你用‘ps’,这个子进程会有一个‘Z’出现在它的状态区
里指出这点。)即使它没有在执行,它仍然占据进程表里一个位置。(它不消耗其
它资源,但是有些工具程序会显示错误的数字,比如中央处理器的使用;这是
因为为节约空间进程表的某些部份与会计数据(accounting info)是共用(overlaid)的。)

这并不好,因为进程表对于进程数有固定的上限,系统会用光它们。即使系统没
有用光 ,每一个用户可以同时执行的进程数有限制,它总是小于系统的限制。
顺便说一下,这也正是你需要总是 检查‘fork()’是否失败的一个原因。

如果父进程未调用wait函数而终止,子进程将被‘init’进程收管,它将控制子进
程退出后必须的清除工作。(‘init’是一个特殊的系统程序,进程号为1 - 它实际
上是系统启动后运行的第一个程序),

1.6.2 我怎样避免它们的出现?
----------------------------

你需要却认父进程为每个子进程的终止调用‘wait()’(或者‘waitpid()’,
‘wait3()’,等等); 或者,在某些系统上,你可以指令系统你对子进程的退出状
态没有兴趣。(译者注:在SysV系统上,可以调用signal函数,设置SIGCLD信号为
SIG_IGN,系统将不产生僵死进程, 详细说明参见<<高级编程>>10.7节)

另一种方法是*两次*‘fork()’,而且使紧跟的子进程直接退出,这样造成孙子进
程变成孤儿进程(orphaned),从而init进程将负责清除它。欲获得做这个的程序,参
看范例章节的函数‘fork2()’。

为了忽略子进程状态,你需要做下面的步骤(查询你的系统手册页以知道这是否正
常工作):

struct sigaction sa;
sa.sa_handler = SIG_IGN;
#ifdef SA_NOCLDWAIT
sa.sa_flags = SA_NOCLDWAIT;
#else
sa.sa_flags = 0;
#endif
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);

如果这是成功的,那么‘wait()’函数集将不再正常工作;如果它们中任何一个被
调用,它们将等待直到*所有*子进程已经退出,然后返回失败,并且
‘errno==ECHILD’。

另一个技巧是捕获SIGCHLD信号,然后使信号处理程序调用‘waitpid()’或
‘wait3()’。参见范例章节的完整程序。

1.7 我怎样使我的程序作为守护程序运行?
======================================

一个“守护程序”进程通常被定义为一个后台进程,而且它不属于任何一个终端
会话,(terminal session)。许多系统服务由守护程序实施;如网络服务,打印等。

简单地在后台启动一个程序并非足够是这些长时间运行的程序;那种方法没有正
确地将进程从启动它的终端脱离(detach)。而且,启动守护程序的普遍接受的的方
法是简单地手工执行或从rc脚本程序执行(译者注:rc:runcom);并希望这个守护
程序将其*自身*安置到后台。

这里是成为守护程序的步骤:

1. 调用‘fork()’以便父进程可以退出,这样就将控制权归还给运行你程序的
命令行或shell程序。需要这一步以便保证新进程不是一个进程组头领进程(process
group leader)。下一步,‘setsid()’,会因为你是进程组头领进程而失败。

2. 调用‘setsid()’ 以便成为一个进程组和会话组的头领进程。由于一个控制终端
与一个会话相关联,而且这个新会话还没有获得一个控制终端,我们的进程没
有控制终端,这对于守护程序来说是一件好事。

3. 再次调用‘fork()’所以父进程(会话组头领进程)可以退出。这意味着我们,一
个非会话组头领进程永远不能重新获得控制终端。

4. 调用‘chdir("/")’确认我们的进程不保持任何目录于使用状态。不做这个会导
致系统管理员不能卸装(umount)一个文件系统,因为它是我们的当前工作目录。

[类似的,我们可以改变当前目录至对于守护程序运行重要的文件所在目录]

5. 调用‘umask(0)’以便我们拥有对于我们写的任何东西的完全控制。我们不知
道我们继承了什么样的umask。

[这一步是可选的](译者注:这里指步骤5,因为守护程序不一定需要写文件)

6. 调用‘close()’关闭文件描述符0,1和2。这样我们释放了从父进程继承的标
准输入,标准输出,和标准错误输出。我们没办法知道这些文描述符符可能
已经被重定向去哪里。注意到许多守护程序使用‘sysconf()’来确认
‘_SC_OPEN_MAX’的限制。‘_SC_OPEN_MAX’告诉你每个进程能够打
开的最多文件数。然后使用一个循环,守护程序可以关闭所有可能的文件描
述符。你必须决定你需要做这个或不做。如果你认为有可能有打开的文件描
述符,你需要关闭它们,因为系统有一个同时打开文件数的限制。

7. 为标准输入,标准输出和标准错误输出建立新的文件描述符。即使你不打算
使用它们,打开着它们不失为一个好主意。准确操作这些描述符是基于各自
爱好;比如说,如果你有一个日志文件,你可能希望把它作为标准输出和标
准错误输出打开,而把‘/dev/null’作为标准输入打开;作为替代方法,你可
以将‘/dev/console’作为标准错误输出和/或标准输出打开,而‘/dev/null’作
为标准输入,或者任何其它对你的守护程序有意义的结合方法。(译者注:一
般使用dup2函数原子化关闭和复制文件描述符,参见<<高级编程>>3.12节)

如果你的守护程序是被‘inetd’启动的,几乎所有这些步骤都不需要(或不建议
采用)。在那种情况下,标准输入,标准输出和标准错误输出都为你指定为网络
连接,而且‘fork()’的调用和会话的操纵不应做(以免使‘inetd’造成混乱)。只
有‘chdir()’和‘umask()’这两步保持有用。

1.8 我怎样象ps程序一样审视系统的进程?
=======================================

你真的不该想做这个。

到目前为止,移植性最好的是调用‘popen(pscmd,"r")’并处理它的输出。(pscmd
应当是类似SysV系统上的‘“ps -ef”’,BSD系统有很多可能的显示选项:选
择一个。)

在范例章节有这个问题的两个完整解决方法;一个适用于SunOS 4,它需要root权
限执行并使用‘kvm_*’例程从内核数据结果读取信息;另一种适用于SVR4系统
(包括Sun OS 5),它使用‘/proc’文件系统。

在具有SVR4.2风格‘/proc’的系统上更简单;只要对于每一个感兴趣的进程号从
文件‘/proc/进程号/psinfo’读取一个psinfo_t结构。但是,这种可能是最清晰的方
法也许又是最不得到很好支持的方法。(在FreeBSD的‘/proc’上,你从
‘/proc/进程号/status’读取一个半未提供文档说明(semi-undocumented)的可打印字
符串;Linux有一些与其类似的东西)

1.9 给定一个进程号,我怎样知道它是个正在运行的程序?
=====================================================

使用‘kill()’函数,而已0作为信号代码(signal number)。

从这个函数返回有四种可能的结果:

* ‘kill()’返回0

- 这意味着一个给定此进程号的进程退出,系统允许你向它发送信号。该进
程是否可以是僵死进程与不同系统有关。

* ‘kill()’返回-1,‘errno == ESRCH’

- 要么不存在给定进程号的进程,要么增强的安全机制导致系统否认它的存
在。(在一些系统上,这个进程有可能是僵死进程。)

* ‘kill()’返回-1,‘errno == EPERM’

- 系统不允许你杀死(kill)这个特定进程。这意味着要么进程存在(它又可能是
僵死进程),要么严格的增强安全机制起作用(比如你的进程不允许发送信号
给*任何人*)。

* ‘kill()’返回-1,伴以其它‘errno’值

- 你有麻烦了!

用的最多的技巧是认为调用“成功”或伴以‘EPERM’的“失败”意味着进程存
在,而其它错误意味着它不存在。

如果你特别为提供‘/proc’文件系统的系统(或所有类似系统)写程序,一个替换
方法存在:检查‘proc/进程号’是否存在是可行的。

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)作为管道名是一种常用的方
法。客户可以先把自己的进程号告诉服务器,然后到那个以自己进程号命名的管
道中读取回复



3. 终端输入/输出
****************

3.1 我怎样使我的程序不回射输入?
================================

我怎样能使我的程序不回射输入,就象登录时询问我的口令时那样?

有一个简单方法,也有一个稍微复杂点的方法:

简单方法是使用‘getpass()’函数,它几乎能在所有Unix系统上找到。它以一个
给定的字符串参数作为提示符(prompt)。它读取输入直到读到一个‘EOF’或换
行符(译者注:‘EOF’用‘^d’输入,而换行符为‘^m’或回车)然后返回一个
指向位于静态内存区包含键入字符的字符串指针。(译者注:字符串不包含换行
符)

复杂一点的方法是使用‘tcgetattr()’函数和‘tcsetattr()’函数,两个函数都使用
一个‘struct termios’结构来操纵终端。下面这两段程序应当能设置回射状态和
不回射状态。

#include <stdlib.h>
#include <stdio.h>

#include <termios.h>
#include <string.h>

static struct termios stored_settings;

void echo_off(void)
{
struct termios new_settings;
tcgetattr(0,&stored_settings);
new_settings = stored_settings;
new_settings.c_lflag &= (~ECHO);
tcsetattr(0,TCSANOW,&new_settings);
return;
}

void echo_on(void)
{
tcsetattr(0,TCSANOW,&stored_settings);
return;
}

两段程序使用到的都是在POSIX标准定义的。

3.2 我怎样从终端读取单个字符?
==============================

我怎样从终端读取单个字符?我的程序总是要等着用户按回车。

终端通常在标准(canonical)模式,在此模式输入总是经编辑后以行读入。你可以
设置终端为非标准(non-canonical)模式,而在此模式下你可以设置在输入传递给
你的程序前读入多少字符。你也可以设定非标准模式的计时器为0,这个计时器
根据设定的时间间隔清空你的缓冲区。这样做使你可以使用‘getc()’函数立即
获得用户的按键输入。我们使用的‘tcgetattr()’函数和‘tcsetattr()’函数都
是在POSIX中定义用来操纵‘termios’结构的。

#include <stdlib.h>
#include <stdio.h>

#include <termios.h>
#include <string.h>

static struct termios stored_settings;

void set_keypress(void)
{
struct termios new_settings;

tcgetattr(0,&stored_settings);

new_settings = stored_settings;

/* Disable canonical mode, and set buffer size to 1 byte */
new_settings.c_lflag &= (~ICANON);
new_settings.c_cc[VTIME] = 0;
new_settings.c_cc[VMIN] = 1;

tcsetattr(0,TCSANOW,&new_settings);
return;
}

void reset_keypress(void)
{
tcsetattr(0,TCSANOW,&stored_settings);
return;
}

3.3 我怎样检查是否一个键被摁下?
================================

我怎样检查是否一个键被摁下?在DOS上我用‘kbhit()’函数,但是在UNIX
上看来没有相同作用的函数?

如果你设定了终端为单一字符模式(参见上一个问题解答),那么(在大多数系统)
上你可以使用‘select()’函数或‘poll()’函数测试输入是否可读。

3.4 我怎样将光标在屏幕里移动?
==============================

我怎样将光标在屏幕里移动?我想不用curses而做全屏编辑。(译者注:curses
是桓鯟/C++编程工具库,它提供编程者许多函数调用,在不用关心终端类型
的情况下操纵终端的显示)。

不开玩笑,你也许不应该想去做这个。Curses工具库知道怎样控制不同终端类型
所表现出的奇特的东西(oddities);当然termcap/terminfo数据会告诉你任何终端类型
具有的这些奇特东西,但你可能会发现正确把握所有这些奇特组合是一件艰巨的
工作。(译者注:在Linux系统上,termcap数据位于/etc/termcap,而terminfo数据位于
/usr/share/terminfo下按不同终端类型首字母存放的不同文件,目前终端类型数已逾
两千种)

但是,你坚决要把你自己搞的手忙脚乱(getting your hands dirty),那么去研究一下
‘termcap’的函数集,特别是‘tputs()’,‘tparm()’和‘tgoto()’函数。

3.5 pttys是什么?
=================

Pseudo-teletypes(pttys, ptys,或其它不同的缩写)是具有两部份的伪设备(pseudo-devices):
一部份为“主人”一边,你可以认为是一个‘用户’,另一部份是“仆人”一边,
它象一个标准的tty设备一样工作。

它们之所以存在是为了提供在程序控制下的一种模拟串行终端行为的方法。比
如,‘telnet’在远端系统使用一个伪终端;服务器的远端登录shell程序只是从“仆
人”一边的tty设备期待着得到操作行为,而在“主人”一边的伪终端由一个守护程
序控制,同时守护程序将所有数据通过网络转发。pttys也被其它程序使用,比如
‘xterm’,‘expect’,‘script’,‘screen’,‘emacs’和其它很多程序。

3.6 怎样控制一个串行口和调制解调器?
====================================

Unix系统下对于串行口的控制很大程度上受串行终端传统的使用影响。以往,
需要不同ioctls函数调用的组合和其它黑客行为才能控制一个串行设备的正确操
作,不过值得庆幸的是,POSIX在这个方面的标准化作了一些努力。

如果你使用的系统不支持‘<termios.h>’头文件,‘tcsetattr()’和其它相关函数,
那么你只能到其它地方去找点资料(或将你的老古董系统升级一下)。

但是不同的系统仍然有显著的区别,主要是在设备名,硬件流控制的操作,和
调制解调器的信号方面。(只要可能,尽量让设备驱动程序去做握手(handshaking)
工作,而不要试图直接操纵握手信号。)

打开和初始华串行设备的基本步骤是:

* 调用‘open()’函数打开设备;而且可能需要使用特定标志作为参数:

`O_NONBLOCK'
除非使用这个标志,否则打开一个供拨入(dial-in)或由调制解调器控制的设
备会造成‘open()’调用被阻塞直到线路接通(carrier is present)。一个非
阻塞的打开操作给你在需要时令调制解调器控制失效的机会。(参见下面的
CLOCAL)

`O_NOCTTY'
在自4.4BSD演化的系统上这个标志是多余的,但在其它系统上它控制串行
设备是否成为会话的控制终端。在大多数情况下你可能不想获得一个控制
终端,所以就要设置这个标志,但是也有例外情况。

* 调用‘tcgetattr()’函数获得当前设备模式。虽然有人会经常取消(ignore)得到的
大多数或全部初始设定,但它仍然不失为一个初始化‘struct termios’结构的
便利方法。

* 设置termios 结构里‘c_iflag’,‘c_oflag’,‘c_flag’,‘c_lfag’和‘c_cc’
为合适的值。(参见下面部分。)

* 调用‘cfsetispeed()’和‘cfsetospeed()’设定设想的波特率。很少系统允许你
设置不同的输入和输出速度,所以普通规律是你需要设成同一个设想的值。

* 调用‘tcsetattr()’设定设备模式。

* 如果你是用‘O_NONBLOCK’标志打开的端口,你可能希望调用‘fcntl()’
函数将‘O_NONBLOCK’标志重新设置成关闭。因为不同系统看来对是否
由非阻塞打开的端口对今后的‘read()’调用造成影响有不同的处理;所以
最好显式地设置好。

一旦你打开并设置了端口,你可以正常地调用‘read()’函数和‘write()’函数。
注意到‘read()’函数的行为将受到你调用‘tcsetattr()’函数时的标志参数设定
的控制。

‘tcflush()’,‘tcdrain()’,‘tcsendbreak()’和‘tcflow()’是其它一些你应当
注意的函数。

当你使用完端口想要关闭时,要注意一些系统上特别恶心的小危险;如果有任何
输出数据等待被写到设备(比如输出流被硬件或软件握手而暂停),你的进程将因
为‘close()’函数调用而被挂起(hang)直到输出数据排空,而且这时的进程是*不
可杀的*(unkillably)。所以调用‘tcflush()’函数丢弃待发数据可能是个明智之举。

(在我的经验中,在tty设备上被阻塞的输出数据是造成不可杀进程最普通的原因。)

3.6.1 串行设备和类型
--------------------

不同系统用于串行端口设备的设备名大相径庭。以下是不同系统的一些例子:

* ‘/dev/tty[0-9][a-z]’作为直接访问设备,而
‘/dev/tty[0-9][A-Z]’ 作为调制解调器控制设备(比如SCO Unix)

* ‘/dev/cua[0-9]p[0-9]’作为直接访问设备,‘/dev/cul[0-9]p[0-9]’作为拨出设
备,而‘/dev/ttyd[0-9]p[0-9]’作为拨入设备(比如HP-UX)

* ‘/dev/cua[a-z][0-9]’作为拨出设备而‘/dev/tty[a-z][0-9]’作为拨入设备(比如
FreeBSD)

是否正确地同所用设备名交互,并在任何硬件握手信号线上产生相应的效果是
与系统,配置和硬件有关的,但是差不多总是遵循下面这些规则(假设硬件是
RS-232 DTE):

- 对于任何设备的一个成功打开操作应当设置DTR和RTS

- 一个对于由调制解调器控制或供拨入的设备的阻塞打开操作将等待DCD(并且
可能DSR和/或CTS也需要)被设置,通常在设置DTR/RTS之后。

- 如果一个对于拨出设备的打开操作正巧赶上一个对于相应拨入设备的打开操作
因为等待线路接通而阻塞,那么打开拨出的操作*也许*造成打开拨入的操作完
成,但*也许也不*造成。一些系统为拨入和拨出端口实现一个简单的共享方案,
当拨出端口在使用时,拨入端口被有效地设置成睡眠状态(“put to sleep”);其
它系统不这样做,在这种系统上为避免竞争(contention)问题,需要外部的协助才
能使拨入和拨出共享端口(比如UUCP锁定文件的使用)。

3.6.2 设置termios的标志位
-------------------------

这里是对你在使用你自己打开的串行设备时设置termios标志的一些提示(即与你使
用已存在的控制tty相反)

3.6.2.1 c_iflag
...............

你也许希望将*所有*‘c_iflag’的标志位设成0,除非你希望使用软件流控制(ick),
在这种情况下你设置‘IXON’和‘IXOFF’。(译者注:有三个标志控制流控制:
IXON,IXOFF ,和IXANY,如果IXON被设置,那么tty输入队列的软件流控制
被设置。当程序无法跟上输入队列的速度,tty传输一个STOP字符,而当输入队
列差不多空时发送START字符。如果IXON被设置,那么tty输出队列的软件流控
制被设置。当tty所连接的设备跟不上输出速度,tty将阻塞程序对tty的写操作。如果
IXANY被设置,那么一旦tty从设备收到任何字符,被暂定的输出将继续 - 译自SCO
Unix 网上文档http://uw7doc.sco.com/SDK_sysprog/CT....html,“TTY flow
control”章节,第五,六段)

3.6.2.2 c_oflag
...............

大部分‘c_oflag’的标志位是为了能使对于慢终端的输出可以正常工作所做的这
样或那样的黑客行为,由此,一些较新的系统认为几乎所有这些标志位已经过
时从而摈弃了它们(特别是所有血淋淋(gory)的输出排列对齐(output-padding)选项)。
如同‘c_iflag’,将它设置成全0对于大部分应用程序来说是合理的。

3.6.2.3 c_cflag
...............

当设置字符的大小时,记住首先使用‘CSIZE’屏蔽,比如设置8位字符,需要:

attr.c_cflag &= ~CSIZE;
attr.c_cflag |= CS8;

在‘c_cflag’里的其它你有可能需要设置为*真*的标志包括‘CREAD’和
‘HUPCL’。

如果你需要产生偶校验,那么设置‘PARENB’并清除‘PARODD’;如果你
需要产生奇校验,那么同时设置‘PARENB’和‘PARODD’。如果你根本不
想设置校验,那么确认清除‘PARENB’。

清除‘CSTOPB’ ,除非你真需要产生两个停止位。

设置硬件流控制的标志可能也能在‘c_cflag’中找到,但它们不是被标准化的(
这是一个遗憾)

3.6.2.4 c_lflag
...............

大部分应用程序可能需要关闭‘ICANON’(标准状态,即基于行的,并进行输
入处理),‘ECHO’和‘ISIG’。

‘IEXTEN’是个更复杂的问题。如果你不把它关闭,具体实现允许你作一些非
标准的事情(比如在‘c_cc’中定义增加的控制字符)从而可能导致不可预料的接
果,但是在一些系统上,你可能需要保持‘IEXTEN’标志为真以得到一些有用
的特征,比如硬件流控制。

3.6.2.5 c_cc
............

这是一个包括输入中带有特殊含义字符的数组。这些字符被命名为‘VINTR’,
‘VSTOP’等等;这些名字是这个数组的索引。

(这些字符中的两个其实根本不是字符,而是当‘ICANON’被关闭时对于
‘read()’函数行为的控制;它们是‘VMIN’和‘VTIME’。)

这些索引名字经常被提及的方式会让人以为它们是实在的变量,比如“设置
VMIN为1” 其实意味着“设置c_cc[VMIN]为1”。这种简写是有用的并且只是
偶尔引起误会。

‘c_cc’的很多变量位置只有当其它标志被设定时才会用到。

只有‘ICANON’被设置,才用到以下变量:
‘VEOF’,‘VEOL’,‘VERASE’,‘VKILL’(如果定义了而且
‘IEXTEN’被设定,那么‘VEOL2’,‘VSTATUS’和‘VWERASE’
也用到)

只有‘ISIG’被设置,才用到以下变量:
‘VINTR’,‘VQUIT’,‘VSUSP’(如果定义了而且‘IEXTEN’被设定,
那么‘VDSUSP’也用到)

只有‘IXON’或‘IXOFF’被设置,才用到以下变量:
‘VSTOP’,‘VSTART’

只有‘ICANON’被取消,才用到以下变量:
‘VMIN’,‘VTIME’

不同系统实现会定义增加的‘c_cc’变量。谨慎的做法是在设定你希望使用的值
以前,使用‘_POSIX_VDISABLE’初始化这些变量(常量‘NCCS’提供这个数
组的大小)

‘VMIN’和‘VTIME’(根据不同的实现方法,它们有可能和‘VEOF’和‘VEOL’
分享相同两个变量)具有以下含义。‘VTIME’的值(如果不为0)总是被解释为以十
分之一秒为单位的计时器)(译者注:VTIME变量是一个字节长,所以1表示0.1秒,
最大为255,表示25.5秒)

`c_cc[VMIN] > 0, c_cc[VTIME] > 0'
只要输入已经有VMIN字节长,或者输入至少有一个字符而在读取最后一个字
符之前VTIME已经过期,或者被信号中断,‘read()’将返回。

`c_cc[VMIN] > 0, c_cc[VTIME] == 0'
只要输入已经有VMIN字节长,或者被信号中断,‘read()’将返回。否则,将
无限等待下去。

`c_cc[VMIN] == 0, c_cc[VTIME] > 0'
只要有输入‘read()’就返回;如果VTIME过期却没有数据,它会返回没有读
到字符。(这和调制解调器挂断时的文件结束标志有一点冲突;使用1作为VMIN,
调用‘alarm()’或‘select()’函数并给定超时参数可以避免这个问题。)

`c_cc[VMIN] == 0, c_cc[VTIME] == 0'
‘read()’总是立刻返回;如果没有数据则返回没有读到字符。(与上面的问题
相同)


4. 系统信息
***********

4.1 怎样知道我的系统有多少存储器容量?
=====================================

这是另一个‘经常未回答的问题’。在多数情况下,你不该试图去找到答案.

如果你必需得到答案,问题的答案通常是有的,但非常依赖于不同的操作系统。
例如,在Solaris中,可以用 `sysconf(_SC_PHYS_PAGES)' 和 `sysconf(_SC_PAGESIZE)';
在FreeBSD中,可以用`sysctl()'; 在Linux中可以通过读取并处理`/proc/meminfo'得到
(使用该文件时需小心你的程序,它要接受历史上任何不同合法格式). 其它的操作
系统有各自的方式,我也没有意识到更多可移植的方法。

在HP-UX(9版和10版)中,可以使用如下的代码:

struct pst_static pst;

if (pstat_getstatic(&pst, sizeof(pst), (size_t) 1, 0) != -1)
{
printf(" Page Size: %lu\n", pst.page_size);
printf("Phys Pages: %lu\n", pst.physical_memory);
}

4.2 我怎样检查一个用户的口令?
=============================

4.2.1 我怎样得到一个用户的口令?
-------------------------------

在多数的UNIX系统中, 用户口令通常存放在`/etc/passwd'文件中. 该文件一般是
这种格式:

用户名:口令:用户号:用户组号:注释:用户目录:登录shell
(usernameassword:uid:gid:gecos field:home directory:login shell)

但是这些标准随着时间而不断改变, 现在的用户信息可能存放在其它机器上, 或
者说并不存放在 `/etc/passwd' 文件中。 当今系统实现也使用 `shadow' 文件保存用
户口令以及一些敏感信息. 该文件只允许有特定权限的用户读取.

为安全考虑,用户口令一般是加密的,而不是用明文表示的。

POSIX 定义了一组访问用户信息的函数. 取得一个用户信息的最快方式是使用`getpwnam()'
和`getpwuid()' 函数. 这两个函数都返回一个结构指针, 该结构包含了用户的所有信
息. `getpwnam()' 接收用户名字符串(username), `getpwuid()' 接收用户号(uid),
(`uid_t'类型在POSIX中有定义). 若调用失败则返回NULL.

但是, 正如前面所讲, 当今的操作系统都有一个shadow文件存放敏感信息,即用户口令。
有些系统当调用者用户号是超级用户时返回用户口令, 其它用户要求你使用其它方式存取
shadow文件. 这时你可以使用`getspnam()', 通过输入用户名得到一个有关用户信息的结
构. 再者, 为了能够成功的完成这些, 你需要有一定的权限. (在一些系统中, 如HP-UX和
SCO, 你可以用`getprpwnam()'代替。)

4.2.2 我怎样通过用户号得到阴影口令文件中的口令?
-----------------------------------------------

我的系统使用一组getsp*函数获得重要用户信息的. 然而, 我没有`getspuid()',
只有`getspnam()'. 我怎样做才能通过用户号获得用户信息呢?

变通方法是相对非常容易的。下面的函数可以直接放入你个人的应用函数库:

#include <stdlib.h>
#include <stdio.h>

#include <pwd.h>
#include <shadow.h>

struct spwd *getspuid(uid_t pw_uid)
{
struct spwd *shadow;
struct passwd *ppasswd;

if( ((ppasswd = getpwuid(pw_uid)) == NULL)
|| ((shadow = getspnam(ppasswd->pw_name)) == NULL))
return NULL;

return shadow;
}

现在的问题是, 有些系统在阴影文件中并不保存用户号(uid)以及其它的信息。

4.2.3 我怎样核对一个用户的口令?
-------------------------------

一个基本的问题是, 存在各种各样的认证系统, 所以口令也就并不总是象它们看上去
那样。 在传统的系统中, 使用UNIX风格的加密算法,加密算法是不同的,有些系统使
用DES(译者注:DES:Data Encryption Standard,为NIST[National Institute of
Standard and Technology]确认的标准加密算法,最新消息表明,NIST将采用一种新
的加密标准Rijndael逐步取代DES)算法,其它的系统, 如FreeBSD国际版使用MD5(译者
注:MD5是当今最为广泛使用的单项散列算法,由Ron Rivest发明,详细资料参见RFC 1321
http://www.faqs.org/rfcs/rfc1321.html)算法。

最常用的方法是使用一种单项加密算法(译者注:即单项散列[Hash]算法)。输入的
明文口令被加密,然后与文件中存放的加密口令比较。怎样加密的详细信息可以
查看`crypt()'的手册页, 这里有一个通常做法的版本:

/* 输入明文口令和加密口令, 检查是否匹配,
* 成功返回 1, 其它情况返回 0。
*/

int check_pass(const char *plainpw, const char *cryptpw)
{
return strcmp(crypt(plainpw,cryptpw), cryptpw) == 0;
}

这个函数之所以能工作是因为加密函数使用的添加(salt)字串存放在加密口令字串的前部。

*警告:* 在一些系统中, 口令加密是使用一种‘crypt()’的变体,即‘bigcrypt()’函数。



 
5. 编程杂技
***********

5.1 我怎样使用通配字符比较字符串?
==================================

对于它的回答依赖于你所谓‘通配字符’一词的确切含义。

有两种很不相同的概念被认定为‘通配字符’。它们是:

*文件名通配模式*(filename patterns)
这是shell用来进行文件名匹配替换的(expansion)(或称为‘globbing’)

*正则表达式*
这是供编辑器用的,比如‘grep’,等等。它是用来匹配正文,而它们正常
情况下不应用于文件名。

5.1.1 我怎样使用文件名通配模式比较字符串?
------------------------------------------

除非你不走运,你的系统应该有函数‘fnmatch()’供你进行文件名匹配。它一
般只允许Bourne Shell风格的模式。它识别‘*’,‘[...]’和‘?’,但可能不
支持在Korn和Bourne-Again shell程序下才有的更神秘(arcane)的模式。

如果你没有这个函数,那么比闭门造车更好的方法是你可以从BSD或GNU原程
序那里去抄(snarfing)一个过来。

而且,在普通的匹配实际文件名情况下,查阅‘glob()’函数,它将搜索到匹配
一个给定模式的所有存在文件。

5.1.2 我怎样使用正则表达式比较字符串?
--------------------------------------

有很多稍有句法不同的正则表达式;大部分系统起码使用两种:一种是‘ed’
程序可以识别的,有时候被记作‘基本正则表达式’,另一种是‘egrep’程序
可以识别的,记作‘扩充正则表达式’。Perl(译者注:Perl: Practical Extract and
Report Language,实用析取与报表语言)语言拥有它自己稍有不同的风格,Emacs
也是。

为了支持这么多格式,相应的有很多实现。系统一般有正则表达式匹配函数(通
常为‘regcomp()’函数和‘regexec()’函数)提供,但是要小心使用;有些系统
有超过一种实现可用,附之以不同的接口。另外,还有很多可用的软件库的实
现(顺便说一下,一般都是将一个正则表达式编译成内部形式然后再使用,因为
总是假设你有很多字符串要比较同一正则表达式。)

一个可用的软件库是‘rx’软件库,从GNU的镜像站点可以得到。它看来是正在
开发中,基于你不同的观点这是一件又好又不好的事情 :-)

5.2 什么是在程序中发送电子邮件的最好方法?
==========================================

有好几种从Unix程序发电子邮件的方法。根据不同情况最好的选择有所不同,
所以我将提供两个方法。还有第三种方法,这里没有说道,是连接本地主机的SMTP
(译者注:SMTP:Simple Mail Transfer Protocol简单邮件传输协议)端口并直接使
用SMTP协议,参见RFC 821(译者注:RFC:Request For Comments)。

5.2.1 简单方法:/bin/mail
-------------------------

对于简单应用,执行‘mail’程序已经是足够了(通常是‘/bin/mail’,但一些系统
上有可能是‘/usr/bin/mail’)。

*警告:*UCB Mail程序的一些版本甚至在非交互模式下也会执行在消息体(message
body)中以‘~!’或‘~|’打头的行所表示的命令。这可能有安全上的风险。

象这样执行:‘mail -s '标题' 收件人地址 ...’,程序将把标准输入作为消息体,
并提供缺省得消息头(其中包括已设定的标题),然后传递整个消息给‘sendmail’
进行投递。

这个范例程序在本地主机上发送一封测试消息给‘root’:

#include <stdio.h>

#define MAILPROG "/bin/mail"

int main()
{
FILE *mail = popen(MAILPROG " -s 'Test Message' root", "w");
if (!mail)
{
perror("popen");
exit(1);
}

fprintf(mail, "This is a test.\n");

if (pclose(mail))
{
fprintf(stderr, "mail failed!\n");
exit(1);
}
}

如果要发送的正文已经保存在一个文件中,那么可以这样做:

system(MAILPROG " -s 'file contents' root </tmp/filename");

这个方法可以扩展到更复杂的情况,但是得当心很多潜在的危险(pitfalls):

* 如果使用system()或popen(),你必须非常当心将参数括起来从而保护它们不被
错误的进行文件名匹配替换或单词分割。

* 基于用户设置数据来构造命令行是缓冲区越界错误和其它安全漏洞的普遍原
因。

* 这种方法不允许设定CC:(译者注:CC:Carbon Copy 抄送)或 BCC:(译者注:
BCC:Blind Carbon Copy:盲送,指投递地址不在消息中出现的抄送)的收件人。
(一些/bin/mail程序的版本允许,其它则不允许)

5.2.2 直接启动邮件传输代理(译者注:MTA: mail transfer agent):/usr/bin/sendmail
-------------------------------------------------------------------------------

‘mail’程序是“邮件用户代理”(Mail User Agent)的一个例子,它旨在供用户
执行以收发电子邮件,但它并不负责实际的传输。一个用来传输邮件的程序被
称为“邮件传输代理”(MTA),而在Unix系统普遍能找到的邮件传输代理被称为
‘sendmail’。也有其它在使用的邮件传输代理,比如‘MMDF’,但这些程序
通常包括一个程序来模拟‘sendmail’的普通做法。

历史上,‘sendmail’通常在‘/usr/lib’里找到,但现在的趋势是将应用库程序从
‘/usr/lib’挪出,并挪入比如‘/usr/sbin’或‘/usr/libexec’等目录。结果是,一般
总是以绝对路径启动‘sendmail’程序,而路径是由系统决定的。

为了了解‘sendmail’程序怎样工作,通常需要了解一下“信封”(envelope)的概
念。这非常类似书面信件;信封上定义这个消息投递给谁,并注明由谁发出(
为了报告错误的目的)。在信封中包含的是“消息头”和“消息体”,之间由一个
空行隔开。消息头的格式主要在RFC 822中提供;并且参见MIME 的RFC文档。(
译者注:MIME的文档包括:RFC1521,RFC1652)

有两种主要的方法使用‘sendmail’程序以生成一个消息:要么信封的收件人能
被显式的提供,要么‘sendmail’程序可被指示从消息头中推理出它们。两种方
法都有优缺点。

5.2.2.1 显式提供信封内容
.........................

消息的信封内容可在命令行上简单的设定。它的缺点在于邮件地址可能包含的
字符会造成‘system()’和‘popen() ’程序可观的以外出错(grief),比如单引号,
被括起的字符串等等。传递这些指令给shell程序并成功解释可以预见潜在的危
险。(可以将命令中任何一个单引号替换成单引号、反斜杠、单引号、单引号的
顺序组合,然后再将整个地址括上单引号。可怕,呃?)

以上的一些不愉快可以通过避开使用‘system()’或‘popen()’函数并求助于‘
fork()’和‘exec()’函数而避免。这有时不管怎样也是需要的;比如,用户
自定义的对于SIGCHLD信号的处理函数通常会打断‘pclose()’函数从而影响到
或大或小的范围。

这里是一个范例程序:

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sysexits.h>
/* #include <paths.h> 如果你有的话 */

#ifndef _PATH_SENDMAIL
#define _PATH_SENDMAIL "/usr/lib/sendmail"
#endif

/* -oi 意味着 "不要视‘ .’为消息的终止符"
* 删除这个选项 ,"--" 如果使用sendmail 8版以前的版本 (并希望没有人
* 曾经使用过一个以减号开头的收件人地址)
* 你也许希望加 -oem (report errors by mail,以邮件方式报告错误)
*/

#define SENDMAIL_OPTS "-oi","--"

/* 下面是一个返回数组中的成员数的宏 */

#define countof(a) ((sizeof(a))/sizeof((a)[0]))

/* 发送由FD所包含以读操作打开的文件之内容至设定的收件人;前提是这
* 个文件中包含RFC822定义的消息头和消息体,收件人列表由NULL指针
* 标志结束;如果发现错误则返回-1,否则返回sendmail的返回值(它使用
* <sysexits.h>中提供的有意义的返回代码)
*/

int send_message(int fd, const char **recipients)
{
static const char *argv_init[] = { _PATH_SENDMAIL, SENDMAIL_OPTS };
const char **argvec = NULL;
int num_recip = 0;
pid_t pid;
int rc;
int status;

/* 计算收件人数目 */

while (recipients[num_recip])
++num_recip;

if (!num_recip)
return 0; /* 视无收件人为成功 */

/* 分配空间给参数矢量 */

argvec = malloc((sizeof char*) * (num_recip+countof(argv_init)+1));
if (!argvec)
return -1;

/* 初始化参数矢量 */

memcpy(argvec, argv_init, sizeof(argv_init));
memcpy(argvec+countof(argv_init),
recipients, num_recip*sizeof(char*));
argvec[num_recip + countof(argv_init)] = NULL;

/* 需要在此增加一些信号阻塞 */

/* 产生子进程 */

switch (pid = fork())
{
case 0: /* 子进程 */

/* 建立管道 */
if (fd != STDIN_FILENO)
dup2(fd, STDIN_FILENO);

/* 其它地方已定义 -- 关闭所有>=参数的文件描述符对应的参数 */
closeall(3);

/* 发送: */
execv(_PATH_SENDMAIL, argvec);
_exit(EX_OSFILE);

default: /* 父进程 */

free(argvec);
rc = waitpid(pid, &status, 0);
if (rc < 0)
return -1;
if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;

case -1: /* 错误 */
free(argvec);
return -1;
}
}

5.2.2.2 允许sendmail程序推理出收件人
.....................................

‘sendmail’的‘-t’选项指令‘sendmail’程序处理消息的头信息,并使用所有
包含收件人(即:‘To:’,‘Cc:’和‘Bcc:’)的头信息建立收件人列表。它的优
点在于简化了‘sendmail’的命令行,但也使得设置在消息头信息中所列以外的
收件人成为不可能。(这通常不是一个问题)

作为一个范例,以下这个程序将标准输入作为一个文件以MIME附件方式发送给
设定的收件人。为简洁起见略去了一些错误检查。这个程序需要调用‘metamail’
分发程序包的‘mimecode’程序。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
/* #include <paths.h> 如果你有的话 */

#ifndef _PATH_SENDMAIL
#define _PATH_SENDMAIL "/usr/lib/sendmail"
#endif

#define SENDMAIL_OPTS "-oi"
#define countof(a) ((sizeof(a))/sizeof((a)[0]))

char tfilename[L_tmpnam];
char command[128+L_tmpnam];

void cleanup(void)
{
unlink(tfilename);
}

int main(int argc, char **argv)
{
FILE *msg;
int i;

if (argc < 2)
{
fprintf(stderr, "usage: %s recipients...\n", argv[0]);
exit(2);
}

if (tmpnam(tfilename) == NULL
|| (msg = fopen(tfilename,"w")) == NULL)
exit(2);

atexit(cleanup);

fclose(msg);
msg = fopen(tfilename,"a");
if (!msg)
exit(2);

/* 建立收件人列表 */

fprintf(msg, "To: %s", argv[1]);
for (i = 2; i < argc; i++)
fprintf(msg, ",\n\t%s", argv[i]);
fputc('\n',msg);

/* 标题 */

fprintf(msg, "Subject: file sent by mail\n");

/* sendmail程序会自动添加 From:, Date:, Message-ID: 等消息头信息 */

/* MIME的处理 */

fprintf(msg, "MIME-Version: 1.0\n");
fprintf(msg, "Content-Type: application/octet-stream\n");
fprintf(msg, "Content-Transfer-Encoding: base64\n");

/* 消息头结束,加一个空行 */

fputc('\n',msg);
fclose(msg);

/* 执行编码程序 */

sprintf(command, "mimencode -b >>%s", tfilename);
if (system(command))
exit(1);

/* 执行信使程序 */

sprintf(command, "%s %s -t <%s",
_PATH_SENDMAIL, SENDMAIL_OPTS, tfilename);
if (system(command))
exit(1);

return 0;
}

Tags:Unix常见问题

很赞哦! ()

文章评论

    共有条评论来说两句吧...

    用户名:

    验证码:

站点信息

  • 建站时间:2019-05-15
  • 文章统计238篇文章
  • 标签管理标签云
  • 统计数据百度统计
  • 公众号:资源连接