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
#include
#include
#include
#include
#include
#include
#include
#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标准的
接口。
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
#include
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
#include
#include
#include
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,一些系统里在中被定
义,[译者注: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
#include
#include
#include
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
#include
#include
#include
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在这个方面的标准化作了一些努力。
如果你使用的系统不支持‘’头文件,‘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/C...ntl.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()’总是立刻返回;如果没有数据则返回没有读到字符。(与上面的问题
相同)
Unix编程常见问题解答《4 系统信息》
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
(username assword: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
#include
#include
#include
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()’函数。
Unix编程常见问题解答《5 编程杂技》
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
#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
这个方法可以扩展到更复杂的情况,但是得当心很多潜在的危险(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
#include
#include
#include
#include
#include
/* #include 如果你有的话 */
#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的返回值(它使用
* 中提供的有意义的返回代码)
*/
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
#include
#include
/* #include 如果你有的话 */
#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;
}
Unix编程常见问题解答《6 工具的使用》
6. 工具的使用
*************
6.1 我怎样调试fork函数产生的子进程?
====================================
根据可用的工具有两种不同的方法:
你的调试器(debugger)可能有允许你选择是否跟踪调用‘fork()’以后的父或子进程
的选项,对于某些目的来说那已经足够了。
替换方法是,你的调试器可能有一个选项允许你将它依附(attach)到一个正在执行
的程序。这样你可以依附调试器到一个已经开始执行的子进程。如果你不需要从
子进程一开始就开始测试,这通常已经足够。否则,你会希望在子进程的‘fork()’
调用后插入一个‘sleep()’调用,或者插入如下的循环:
{
volatile int f = 1;
while(f);
}
这样子进程将一直在此循环不往下执行直到你用调试器设定‘f’为0。
并且记住,使用调试器并非是找到你程序中错误的唯一方法;在很多Unix系统
上有一些工具程序可用来跟踪系统调用和信号,而且丰富的日志经常也是有
用的。
6.2 怎样通过其他库文件建立新的库文件?
======================================
前提是我们所说的是归档(archive)(静态)库,最简单的方法是将所有选择的库
文件使用‘ar x’命令在一个空目录里拆分成它们原始的单个目标文件(object),
然后再合并成一个。当然,文件名有可能会重复,但是如果这个库文件很大,
你也许一开始就不想将它们合并在一起。
6.3 怎样创建动态连接库(shared library)/dlls?
=============================================
创建动态连接库(shared libraries)的方法根据不同的系统有所不同。这个过程主要
分两步;第一步要求包括在动态连接库中的目标必须首先是编译好的,通常需
要某个编译选项指示这串代码是位置无关的(position-indepenent);第二步,是将
这些目标连接在一起形成一个库文件。
这里是一个演示以上道理的小程序:
/* shrobj.c 文件 */
const char *myfunc()
{
return "Hello World";
}
/* shrobj.c 结束 */
/* hello.c 文件 */
#include
extern const char *myfunc();
main()
{
printf("%s/n", myfunc());
return 0;
}
/* hello.c 结束 */
$ gcc -fpic -c shrobj.c
$ gcc -shared -o libshared.so shrobj.o
$ gcc hello.c libshared.so
$ ./a.out
Hello World
到目前为止,如果你希望库文件和它的创建过程是都可以移植的话,那么最好
的办法是使用GNU Libtool程序包。它是个小型的工具程序套件,这些工具程序
知道建立动态连接库的平台无关性;你可以只发布你的程序必要的部分,从而
当一个安装者配置你的软件包时,他能决定生成什么库。Libtool程序包在不支持
动态连接库的系统上也能工作得很好。它而且知道与GNU Autoconf程序和GNU
Automake程序挂钩(如果你使用这些工具来管理你程序的编译创建过程)。
如果你不想使用Libtool程序包,那么对于gcc以外的编译器,你需要按照下面所
列修改编译器参数:
AIX 3.2 使用 xlc (未证实)
取消‘-fpic’选项,以‘-bM:SRE -bE:libshared.exp’取代‘-shared’。你并且
需要创建一个名为‘libshared.exp’的文件保存一个所有输出符号(symbols to export)
的列表,比如以上的范例程序,就需要输出‘myfunc’符号。另外,在连接库
时使用‘-e _nostart’参数(在较新的AIX版本上,我相信应该将其变成‘-bnoentry’)。
SCO OpenServer 5 使用 SCO 开发系统(Development System) (未证实)
如果你使用ELF(译者注:ELF:执行与连接格式Executable and Linking Forrmat,
一种Unix可执行目标文件的格式)格式,那么共享库只能在OS5上可用,而它
需要‘-belf’选项。并以‘-Kpic’取代‘-fpic’,在连接时使用‘cc -belf -G’。
Solaris 使用 SparcWorks 编译器
以‘-pic’取代‘-fpic’,并以‘ld -G’取代‘gcc -shared’。
(鼓励大家提供更多的材料丰富上述列表)
其它要当心的问题:
* AIX和(我相信)Digital Unix不需要-fpic选项,因为所有代码都是位置无关的。
* AIX一般需要你创建一个‘输出文件’,即一个保存所有动态连接库中输出
符号的列表。一些连接器(linker)版本(可能只有SLHS连接器,是svld?)有一个
选项可以输出所有符号。
* 如果你对于连接器想使用普遍的‘-l’参数来引用你的动态连接库,你必须
理解你的系统在实际运行时是怎样寻找动态连接库的。最普通的方法是使用
‘LD_LIBRARY_PATH’环境变量,但是通常在连接时有一种其它选项可以
设定。
* 大多数实现方法是在程序内部记录所希望的动态连接库在运行时的位置。这
样把一个动态连接库从一个目录移到另一个目录将导致程序无法工作。许多
系统对于连接器有一个选项用以设定希望运行时动态连接库的位置(比如在
Solaris系统上是‘-R’连接器选项,或者是‘LD_RUN_PATH’环境变量)。
* ELF和a.out的实现方法可能有一个连接器选项‘-Bsymbolic’,它导致在库本
身内部的引用被解释好。否则,在这些系统上,所有符号的解释将推迟到最
后连接时再进行,这样在main程序中的单一函数将重载库中的对应函数。
6.4 我能更改一个动态连接库里的目标吗?
======================================
一般不行。
在大多数系统上(除了AIX),当你连接目标并形成一个动态连接库时,它就象连
接一个可执行程序;目标并不保留它们单一的特征。结果是,一般不能从一个动
态连接库里析取出或更换一个单一的目标。
6.5 我能在一个运行着的程序中生成堆栈映象吗?
============================================
一些系统提供库函数可以提取(unwinding)出堆栈,从而(比如)你可以在一个错误
处理函数中生成一个堆栈映象,但是只有一小部分系统有这些函数。
一个可能的变通方法(workaround)是让你的程序执行一个调试器调试*它自己* -
详细方法仍然根据不同系统稍有不同,但一般的概念是这样:
void dump_stack(void)
{
char s[160];
sprintf(s, "/bin/echo 'where/ndetach' | dbx -a %d", getpid());
system(s);
return;
}
你需要根据你不同的系统对dbx的参数和命令进行加工,或者甚至换另一个调试
器,比如‘gdb’,但这仍然是我见过的对于这种问题最普遍的解决方法。为此,
荣誉授予Ralph Corderoy。
下面列表包含在一些系统上需要用到的命令行:
大多数使用dbx的系统
`"/bin/echo 'where/ndetach' | dbx /path/to/program %d"'
AIX
`"/bin/echo 'where/ndetach' | dbx -a %d"'
IRIX
`"/bin/echo 'where/ndetach' | dbx -p %d"'
?
范例程序
********
捕获 SIGCHLD 信号
=================
#include /* 在任何其它 sys 下的头文件之前引用这个头文件 */
#include /* waitpid()和一些不同的宏所需的头文件 */
#include /* 信号函数的头文件 */
#include /* fprintf函数的头文件 */
#include /* fork函数的头文件 */
void sig_chld(int); /* 我们的 SIGCHLD 信号处理函数的原形(prototype) */
int main()
{
struct sigaction act;
pid_t pid;
/* 设定sig_chld函数作为我们SIGCHLD信号的处理函数 */
act.sa_handler = sig_chld;
/* 在这个范例程序里,我们不想阻塞其它信号 */
sigemptyset(&act.sa_mask);
/*
* 我们只关谋恢罩沟淖咏蹋皇潜恢卸? * 的子进程 (比如用户在终端上按Control-Z)
*/
act.sa_flags = SA_NOCLDSTOP;
/*
* 使这些设定的值生效. 如果我们是写一个真实的应用程序,
* 我们也许应该保存这些原有值,而不是传递一个NULL。
*/
if (sigaction(SIGCHLD, &act, NULL) < 0)
{
fprintf(stderr, "sigaction failed/n");
return 1;
}
/* fork */
switch (pid = fork())
{
case -1:
fprintf(stderr, "fork failed/n");
return 1;
case 0: /* 是子进程,直接结束 */
_exit(7); /* 退出状态 = 7 */
default: /* 父进程 */
sleep(10); /* 给子进程完成的时间 */
}
return 0;
}
/*
* 信号处理函数 -- 只有当接收到一个SIGCHLD信号才被调用,
* 即有一个子进程终止
*/
void sig_chld(int signo)
{
int status, child_val;
/* 非阻塞地等待任何子进程结束 */
if (waitpid(-1, &status, WNOHANG) < 0)
{
/*
* 不建议在信号处理函数中调用标准输入/输出函数,
* 但在一个类似这个的玩具程序里或许没问题
*/
fprintf(stderr, "waitpid failed/n");
return;
}
/*
* 我们现在有保存在‘status’变量中的子进程退出信息并可以使用
* wait.h中定义的宏对其进行操作
*/
if (WIFEXITED(status)) /* 子进程是正常退出吗? */
{
child_val = WEXITSTATUS(status); /* 获取子进程的退出状态 */
printf("child's exited normally with status %d/n", child_val);
}
}
读取进程表 - SUNOS 4 版
=======================
#define _KMEMUSER
#include
#include
#include
char regexpstr[256];
#define INIT register char *sp=regexpstr;
#define GETC() (*sp++)
#define PEEKC() (*sp)
#define UNGETC(c) (--sp)
#define RETURN(pointer) return(pointer);
#define ERROR(val)
#include
pid_t
getpidbyname(char *name,pid_t skipit)
{
kvm_t *kd;
char **arg;
int error;
char *p_name=NULL;
char expbuf[256];
char **freeme;
int curpid;
struct user * cur_user;
struct user myuser;
struct proc * cur_proc;
if((kd=kvm_open(NULL,NULL,NULL,O_RDONLY,NULL))==NULL){
return(-1);
}
sprintf(regexpstr,"^.*/%s$",name);
compile(NULL,expbuf,expbuf+256,'/0');
while(cur_proc=kvm_nextproc(kd)){
curpid = cur_proc->p_pid;
if((cur_user=kvm_getu(kd,cur_proc))!=NULL){
error=kvm_getcmd(kd,cur_proc,cur_user,&arg,NULL);
if(error==-1){
if(cur_user->u_comm[0]!='/0'){
p_name=cur_user->u_comm;
}
}
else{
p_name=arg[0];
}
}
if(p_name){
if(!strcmp(p_name,name)){
if(error!=-1){
free(arg);
}
if(skipit!=-1 && ourretval==skipit){
ourretval=-1;
}
else{
close(fd);
break;
}
break;
}
else{
if(step(p_name,expbuf)){
if(error!=-1){
free(arg);
}
break;
}
}
}
if(error!=-1){
free(arg);
}
p_name=NULL;
}
kvm_close(kd);
if(p_name!=NULL){
return(curpid);
}
return (-1);
}
读取进程表 - SYSV 版
====================
pid_t
getpidbyname(char *name,pid_t skipit)
{
DIR *dp;
struct dirent *dirp;
prpsinfo_t retval;
int fd;
pid_t ourretval=-1;
if((dp=opendir("/proc"))==NULL){
return -1;
}
chdir("/proc");
while((dirp=readdir(dp))!=NULL){
if(dirp->d_name[0]!='.'){
if((fd=open(dirp->d_name,O_RDONLY))!=-1){
if(ioctl(fd,PIOCPSINFO,&retval)!=-1){
if(!strcmp(retval.pr_fname,name)){
ourretval=(pid_t)atoi(dirp->d_name);
if(skipit!=-1 && ourretval==skipit){
ourretval=-1;
}
else{
close(fd);
break;
}
}
}
close(fd);
}
}
}
closedir(dp);
return ourretval;
}
读取进程表 - AIX 4.2 版
=======================
#include
#include
int getprocs(struct procsinfo *, int, struct fdsinfo *,
int, pid_t *, int);
pid_t getpidbyname(char *name, pid_t *nextPid)
{
struct procsinfo pi;
pid_t retval = (pid_t) -1;
pid_t pid;
pid = *nextPid;
while(1)
{
if(getprocs(&pi, sizeof pi, 0, 0, &pid, 1) != 1)
break;
if(!strcmp(name, pi.pi_comm))
{
retval = pi.pi_pid;
*nextPid = pid;
break;
}
}
return retval;
}
int main(int argc, char *argv[])
{
int curArg;
pid_t pid;
pid_t nextPid;
if(argc == 1)
{
printf("syntax: %s [program ...]/n",argv[0]);
exit(1);
}
for(curArg = 1; curArg < argc; curArg++)
{
printf("Process IDs for %s/n", argv[curArg]);
for(nextPid = 0, pid = 0; pid != -1; )
if((pid = getpidbyname(argv[curArg], &nextPid)) != -1)
printf("/t%d/n", pid);
}
}
使用popen函数和ps命令读取进程表
===============================
#include /* FILE, sprintf, fgets, puts */
#include /* atoi, exit, EXIT_SUCCESS */
#include /* strtok, strcmp */
#include /* pid_t */
#include /* WIFEXITED, WEXITSTATUS */
char *procname(pid_t pid)
{
static char line[133], command[80], *linep, *token, *cmd;
FILE *fp;
int status;
if (0 == pid) return (char *)0;
sprintf(command, "ps -p %d 2>/dev/null", pid);
fp = popen(command, "r");
if ((FILE *)0 == fp) return (char *)0;
/* 读取标题行 */
if ((char *)0 == fgets(line, sizeof line, fp))
{
pclose(fp);
return (char *)0;
}
/* 从标题栏分析出命令名所在列。
* (BSD风格的系统将指示命令的"COMMAND"字符串放在第5列,SysV好象将
* 指示命令的“CMD”或“COMMAND”字符串放在第4列)
*/
for (linep = line; ; linep = (char *)0)
{
if ((char *)0 == (token = strtok(linep, " /t/n")))
{
pclose(fp);
return (char *)0;
}
if (0 == strcmp("COMMAND", token) || 0 == strcmp("CMD", token))
{ /* 我们找到COMMAND所在列 */
cmd = token;
break;
}
}
/* 读取 ps(1) 输出行 */
if ((char *)0 == fgets(line, sizeof line, fp))
{
pclose(fp);
return (char *)0;
}
/* 抓COMMAND标题下面的词 ... */
if ((char *)0 == (token = strtok(cmd, " /t/n")))
{
pclose(fp);
return (char *)0;
}
status = pclose(fp);
if (!WIFEXITED(status) || 0 != WEXITSTATUS(status))
return (char *)0;
return token;
}
int main(int argc, char *argv[])
{
puts(procname(atoi(argv[1])));
exit(EXIT_SUCCESS);
}
守护程序工具函数
================
#include
#include
#include
#include
#include
#include
#include
/* closeall() -- 关闭所有>=给定值的文件描述符 */
void closeall(int fd)
{
int fdlimit = sysconf(_SC_OPEN_MAX);
while (fd < fdlimit)
close(fd++);
}
/* daemon() - 将进程从用户端脱离并消失进入后台,若失败返回-1,
* 但是在那种情况下你只能退出,因为我们可能已经生成了子进程。
* 这是基于BSD的版本,所以调用方需负责类似umask等等其它的工作。
*/
/* 相信在所有Posix系统上都能工作 */
int daemon(int nochdir, int noclose)
{
switch (fork())
{
case 0: break;
case -1: return -1;
default: _exit(0); /* 原进程退出 */
}
if (setsid() < 0) /* 不应该失败 */
return -1;
/* 如果你希望将来获得一个控制tty,则排除(dyke)以下的switch语句 */
/* -- 正常情况不建议用于守护程序 */
switch (fork())
{
case 0: break;
case -1: return -1;
default: _exit(0);
}
if (!nochdir)
chdir("/");
if (!noclose)
{
closeall(0);
open("/dev/null",O_RDWR);
dup(0); dup(0);
}
return 0;
}
/* fork2() -- 类似fork函数,但子进程立刻变成孤儿进程
* (当它退出时不产生僵死进程)
* 返回1给父进程,不是任何有意义的进程号.
* 父进程不能使用wait函数等待子进程结束 (它们是无关的).
*/
/* 这个版本假设你没有捕获和忽略SIGCHLD信号. */
/* 如果你有设定,则不管怎样应使用fork函数 */
int fork2()
{
pid_t pid;
int rc;
int status;
if (!(pid = fork()))
{
switch (fork())
{
case 0: return 0;
case -1: _exit(errno); /* 假设错误码都小于256 */
default: _exit(0);
}
}
if (pid < 0 || waitpid(pid,&status,0) < 0)
return -1;
if (WIFEXITED(status))
if (WEXITSTATUS(status) == 0)
return 1;
else
errno = WEXITSTATUS(status);
else
errno = EINTR; /* 唉,类似这个 :-) */
return -1;
}
一个使用以上函数的范例程序:
#include
#include
#include
#include
#include
#include
#include
int daemon(int,int);
int fork2(void);
void closeall(int);
#define TCP_PORT 8888
void errexit(const char *str)
{
syslog(LOG_INFO, "%s failed: %d (%m)", str, errno);
exit(1);
}
void errreport(const char *str)
{
syslog(LOG_INFO, "%s failed: %d (%m)", str, errno);
}
/* 实际的子进程在此. */
void run_child(int sock)
{
FILE *in = fdopen(sock,"r");
FILE *out = fdopen(sock,"w");
int ch;
setvbuf(in, NULL, _IOFBF, 1024);
setvbuf(out, NULL, _IOLBF, 1024);
while ((ch = fgetc(in)) != EOF)
fputc(toupper(ch), out);
fclose(out);
}
/* 这是守护程序的主要工作 -- 侦听连接并生成子进程 */
void process()
{
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int sock = socket(AF_INET, SOCK_STREAM, 0);
int flag = 1;
int rc = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
&flag, sizeof(flag));
if (rc < 0)
errexit("setsockopt");
addr.sin_family = AF_INET;
addr.sin_port = htons(TCP_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
rc = bind(sock, (struct sockaddr *) &addr, addrlen);
if (rc < 0)
errexit("bind");
rc = listen(sock, 5);
if (rc < 0)
errexit("listen");
for (;;)
{
rc = accept(sock, (struct sockaddr *) &addr, &addrlen);
if (rc >= 0)
switch (fork2())
{
case 0: close(sock); run_child(rc); _exit(0);
case -1: errreport("fork2"); close(rc); break;
default: close(rc);
}
}
}
int main()
{
if (daemon(0,0) < 0)
{
perror("daemon");
exit(2);
}
openlog("test", LOG_PID, LOG_DAEMON);
process();
return 0;
}
调制解调器控制范例程序
======================
/* 发出一些简单调制解调器命令
* 需要串行设备的设备名 (最好是拨出设备,
* 或者是非调制解调器控制设备) 作为它唯一的参数.
* 如果你没有可共使用的拨出设备, 那么以CFLAGS_TO_SET取代CLOCAL。
*/
#include
#include
#include
#include
#include
#include
#include /* 也许需要;和系统有关 */
#include
#include
#include
#include
#define CFLAGS_TO_SET (CREAD | HUPCL)
#define CFLAGS_TO_CLEAR (CSTOPB | PARENB | CLOCAL)
enum flowmode { NoFlow, HardFlow, SoftFlow };
/* 和系统有关 */
#define CFLAGS_HARDFLOW (CRTSCTS)
#define EXAMPLE_BAUD B19200
#define EXAMPLE_FLOW HardFlow
static void die(const char *msg)
{
fprintf(stderr, "%s/n", msg);
exit(1);
}
static int close_and_complain(int fd, const char *msg, int err)
{
fprintf(stderr, "%s: %s/n", msg, strerror(err));
if (fd >= 0)
close(fd);
errno = err;
return -1;
}
int open_port(const char *name, speed_t baud, enum flowmode flow)
{
int flags;
struct termios attr;
int fd = open(name, O_RDWR | O_NONBLOCK | O_NOCTTY);
if (fd < 0)
return close_and_complain(-1, "open", errno);
/* 设定一些不明确是否敏感的值 */
if (tcgetattr(fd, &attr) < 0)
return close_and_complain(fd, "tcgetattr", errno);
/* 无特殊输入或输出处理 */
attr.c_iflag = (flow == SoftFlow) ? (IXON | IXOFF) : 0;
attr.c_oflag = 0;
/* 设定8位字符宽和一些杂项控制模式 */
attr.c_cflag &= ~(CSIZE | CFLAGS_TO_CLEAR | CFLAGS_HARDFLOW);
attr.c_cflag |= (CS8 | CFLAGS_TO_SET);
if (flow == HardFlow)
attr.c_cflag |= CFLAGS_HARDFLOW;
/* 本机模式 */
attr.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ISIG);
/* 特殊字符 -- 许多已被先前的设定取消 */
{
int i;
#ifdef _POSIX_VDISABLE
attr.c_cc[0] = _POSIX_VDISABLE;
#else
attr.c_cc[0] = fpathconf(fd, _PC_VDISABLE);
#endif
for (i = 1; i < NCCS; i++)
attr.c_cc[i] = attr.c_cc[0];
}
attr.c_cc[VSTART] = 0x11;
attr.c_cc[VSTOP] = 0x13;
/* 对read()函数的计时控制 */
attr.c_cc[VMIN] = 1;
attr.c_cc[VTIME] = 0;
/* 波特律 */
cfsetispeed(&attr, baud);
cfsetospeed(&attr, baud);
/* 写入设定 */
if (tcsetattr(fd, TCSANOW, &attr) < 0)
return close_and_complain(fd, "tcsetattr", errno);
/* 如果系统记住了先前的O_NONBLOCK设定,就取消它 */
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
return close_and_complain(fd, "fcntl(GETFL)", errno);
if (fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0)
return close_and_complain(fd, "fcntl(SETFL)", errno);
return fd;
}
/* 一些简单的计时工具函数 */
/* 向*TV加 SECS 和USECS */
static void timeradd(struct timeval *tv, long secs, long usecs)
{
tv->tv_sec += secs;
if ((tv->tv_usec += usecs) >= 1000000)
{
tv->tv_sec += tv->tv_usec / 1000000;
tv->tv_usec %= 1000000;
}
}
/* 设定 *RES = *A - *B, 返回结果的符号 */
static int timersub(struct timeval *res,
const struct timeval *a, const struct timeval *b)
{
long sec = a->tv_sec - b->tv_sec;
long usec = a->tv_usec - b->tv_usec;
if (usec < 0)
usec += 1000000, --sec;
res->tv_sec = sec;
res->tv_usec = usec;
return (sec < 0) ? (-1) : ((sec == 0 && usec == 0) ? 0 : 1);
}
/* 这个函数不试图处理非正常的字符串 (比如 ababc)
* 超时以微妙计
* 一个更通常的做法是使用alarm()函数处理超时.
* 这个函数为简便起见不使用信号处理并试图提供一种替换方法
*/
int expect(int fd, const char *str, int timeo)
{
int matchlen = 0;
int len = strlen(str);
struct timeval now,end,left;
fd_set fds;
char c;
gettimeofday(&end, NULL);
timeradd(&end, timeo/1000, timeo%1000);
while (matchlen < len)
{
gettimeofday(&now, NULL);
if (timersub(&left, &end, &now) <= 0)
return -1;
FD_ZERO(&fds);
FD_SET(fd, &fds);
if (select(fd+1, &fds, NULL, NULL, &left) <= 0)
return -1;
if (read(fd, &c, 1) != 1)
return -1;
if (isprint((unsigned char)c) || c == '/n' || c == '/r')
putchar(c);
else
printf("//x%02x", c);
if (c == str[matchlen])
++matchlen;
else
matchlen = 0;
}
return 0;
}
int main(int argc, char **argv)
{
int fd;
unsigned char c;
if (argc < 2)
die("no port specified");
setvbuf(stdout, NULL, _IONBF, 0);
fd = open_port(argv[1], EXAMPLE_BAUD, EXAMPLE_FLOW);
if (fd < 0)
die("cannot open port");
write(fd, "AT/r", 3);
if (expect(fd, "OK", 5000) < 0)
{
write(fd, "AT/r", 3);
if (expect(fd, "OK", 5000) < 0)
{
tcflush(fd, TCIOFLUSH);
close(fd);
die("no response to AT");
}
}
write(fd, "ATI4/r", 5);
expect(fd, "OK", 10000);
putchar('/n');
tcflush(fd, TCIOFLUSH);
close(fd);
return 0;
}
事务控制范例程序
================
/* 生成前台/后台事务的函数 */
#include
#include
#include
#include
#include
#include
#include
#include
/* 一些下面的函数会因为无法定位控制tty和调用方不在前台而失败。
* 第一种情况时,我们假设一个前台程序会有为标准输入,标准输出或标准错误输出打开的ctty,
* 而如果没有则返回ENOTTY。
* 第二种情况时,除foreground_self()函数的特殊情况以外,
* 若一个非前台程序打算输出一些东西到前台,我们返回EPERM。
* (也许想得太多了)
*/
/* 为给定的pgrp安排一个终端 (打开一个ctty) .
* 这个tcsetpgrp()外壳程序只是因为POSIX中特别错误(bogusity)的地方而需要;
* 遵照标准的系统在一个非前台进程调用tcsetpgrp函数时传递SIGTTOU
* 信号(差不多总是这样)。这是虚假的一致性之于一般想法的胜利。
*/
int assign_terminal(int ctty, pid_t pgrp)
{
sigset_t sigs;
sigset_t oldsigs;
int rc;
sigemptyset(&sigs);
sigaddset(&sigs,SIGTTOU);
sigprocmask(SIG_BLOCK, &sigs, &oldsigs);
rc = tcsetpgrp(ctty, pgrp);
sigprocmask(SIG_SETMASK, &oldsigs, NULL);
return rc;
}
/* 类似fork函数,但做事务控制。如果新建立的进程放在前台则设fg为真。
* (这样隐式地将调用方进程放置到后台,所以做完这个后要当心tty的输入/输出)
* 设定pgrp为-1以创建一个新事务,在此情况下返回的进程号即是新事务的进程组号,
* 或者设定一个同一会话中存在的事务(一般只用来启动管道操作的第二个或第二个以后
* 的进程)。
*/
pid_t spawn_job(int fg, pid_t pgrp)
{
int ctty = -1;
pid_t pid;
/* 如果生成一个*新*的前台事务,起码要求标准输入,标准输出或
* 标准错误输出的其中一个指向的是控制tty,并且当前进程在前台。
* 只有当在存在事务中开始一个新前台进程时才检查控制中的tty。
* 一个没有控制tty的会话只能有后台事务。
*/
if (fg)
{
pid_t curpgrp;
if ((curpgrp = tcgetpgrp(ctty = 2)) < 0
&& (curpgrp = tcgetpgrp(ctty = 0)) < 0
&& (curpgrp = tcgetpgrp(ctty = 1)) < 0)
return errno = ENOTTY, (pid_t)-1;
if (pgrp < 0 && curpgrp != getpgrp())
return errno = EPERM, (pid_t)-1;
}
switch (pid = fork())
{
case -1: /* fork失败 */
return pid;
case 0: /* 子进程 */
/* 建立新进程组, 如果需要则将我们放到前台
* 不知道如果setpgid函数调用失败该怎么办(“不会发生”)
*/
if (pgrp < 0)
pgrp = getpid();
if (setpgid(0,pgrp) == 0 && fg)
assign_terminal(ctty, pgrp);
return 0;
default: /* 父进程 */
/* 这里也建立自进程组. */
if (pgrp < 0)
pgrp = pid;
setpgid(pid, pgrp);
return pid;
}
/*不会执行到这里*/
}
/* 用SIGNO表示的信号杀死PGRP表示的事务 */
int kill_job(pid_t pgrp, int signo)
{
return kill(-pgrp, signo);
}
/* 中断PGRP表示的事务 */
int suspend_job(pid_t pgrp)
{
return kill_job(pgrp, SIGSTOP);
}
/* 继续在后台执行PGRP表示的事务 */
int resume_job_bg(pid_t pgrp)
{
return kill_job(pgrp, SIGCONT);
}
/* 继续在前台执行PGRP表示的事务 */
int resume_job_fg(pid_t pgrp)
{
pid_t curpgrp;
int ctty;
if ((curpgrp = tcgetpgrp(ctty = 2)) < 0
&& (curpgrp = tcgetpgrp(ctty = 0)) < 0
&& (curpgrp = tcgetpgrp(ctty = 1)) < 0)
return errno = ENOTTY, (pid_t)-1;
if (curpgrp != getpgrp())
return errno = EPERM, (pid_t)-1;
if (assign_terminal(ctty, pgrp) < 0)
return -1;
return kill_job(pgrp, SIGCONT);
}
/* 将我们自己放置到前台,比如在中断一个前台事务之后调用
*/
int foreground_self()
{
pid_t curpgrp;
int ctty;
if ((curpgrp = tcgetpgrp(ctty = 2)) < 0
&& (curpgrp = tcgetpgrp(ctty = 0)) < 0
&& (curpgrp = tcgetpgrp(ctty = 1)) < 0)
return errno = ENOTTY, (pid_t)-1;
return assign_terminal(ctty, getpgrp());
}
/* closeall() - 关闭所有>=给定FD的文件描述符 */
void closeall(int fd)
{
int fdlimit = sysconf(_SC_OPEN_MAX);
while (fd < fdlimit)
close(fd++);
}
/* 类似system()函数,但将给定的命令作为后台事务执行,返回shell进程
* 的进程号(并且也是这个事务的进程组号,适用于kill_job等等)。
* 如果参数INFD,OUTFD或ERRFD为非NULL,则打开一个管道和一个文件描述
* 符保存与该管道有关的父进程端,然后在子进程中将被从定向到/dev/null。
* 并且在子进程中关闭所有>2的文件描述符(一个经常过份估计的工作)
*/
pid_t spawn_background_command(const char *cmd,
int *infd, int *outfd, int *errfd)
{
int nullfd = -1;
int pipefds[3][2];
int error = 0;
if (!cmd)
return errno = EINVAL, -1;
pipefds[0][0] = pipefds[0][1] = -1;
pipefds[1][0] = pipefds[1][1] = -1;
pipefds[2][0] = pipefds[2][1] = -1;
if (infd && pipe(pipefds[0]) < 0)
error = errno;
else if (outfd && pipe(pipefds[1]) < 0)
error = errno;
else if (errfd && pipe(pipefds[2]) < 0)
error = errno;
if (!error && !(infd && outfd && errfd))
{
nullfd = open("/dev/null",O_RDWR);
if (nullfd < 0)
error = errno;
}
if (!error)
{
pid_t pid = spawn_job(0, -1);
switch (pid)
{
case -1: /* fork失败 */
error = errno;
break;
case 0: /* 子进程 */
dup2(infd ? pipefds[0][0] : nullfd, 0);
dup2(outfd ? pipefds[1][1] : nullfd, 1);
dup2(errfd ? pipefds[2][1] : nullfd, 2);
closeall(3);
execl("/bin/sh","sh","-c",cmd,(char*)NULL);
_exit(127);
default: /* 父进程 */
close(nullfd);
if (infd)
close(pipefds[0][0]), *infd = pipefds[0][1];
if (outfd)
close(pipefds[1][1]), *outfd = pipefds[1][0];
if (errfd)
close(pipefds[2][1]), *errfd = pipefds[2][0];
return pid;
}
}
/* 只在错误时执行到这里 */
{
int i,j;
for (i = 0; i < 3; ++i)
for (j = 0; j < 2; ++j)
if (pipefds[i][j] >= 0)
close(pipefds[i][j]);
}
if (nullfd >= 0)
close(nullfd);
return errno = error, (pid_t) -1;
}
/*---------------------------------------*/
/* 这里是使用上述函数一个小例子. */
pid_t bgjob = -1;
volatile int signo = 0;
#ifndef WCOREDUMP
/* 如果没有 WCOREDUMP, 你也许会希望在你的平台上为它设置一个准确的定义
* (这通常是(status & 0x80) 但也不总是这样),或者就赌没有core dumps(
* 就象这个程序所做)
*/
# define WCOREDUMP(status) (0)
#endif
int check_children()
{
pid_t pid;
int status;
int count = 0;
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if (pid == bgjob && !WIFSTOPPED(status))
bgjob = -1;
++count;
if (WIFEXITED(status))
fprintf(stderr,"Process %ld exited with return code %d/n",
(long)pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
fprintf(stderr,"Process %ld killed by signal %d%s/n",
(long)pid, WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
else if (WIFSTOPPED(status))
fprintf(stderr,"Process %ld stopped by signal %d/n",
(long)pid, WSTOPSIG(status));
else
fprintf(stderr,"Unexpected status - pid=%ld, status=0x%x/n",
(long)pid, status);
}
return count;
}
void sighandler(int sig)
{
if (sig != SIGCHLD)
signo = sig;
}
int main()
{
struct sigaction act;
int sigcount = 0;
act.sa_handler = sighandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT,&act,NULL);
sigaction(SIGQUIT,&act,NULL);
sigaction(SIGTERM,&act,NULL);
sigaction(SIGTSTP,&act,NULL);
sigaction(SIGCHLD,&act,NULL);
fprintf(stderr,"Starting background job 'sleep 60'/n");
bgjob = spawn_background_command("sleep 60", NULL, NULL, NULL);
if (bgjob < 0)
{
perror("spawn_background_command");
exit(1);
}
fprintf(stderr,"Background job started with id %ld/n", (long)bgjob);
while (bgjob >= 0)
{
if (signo)
{
fprintf(stderr,"Signal %d caught/n", signo);
if (sigcount++)
kill_job(bgjob, SIGKILL);
else
{
kill_job(bgjob, SIGTERM);
kill_job(bgjob, SIGCONT);
}
}
if (!check_children())
pause();
}
fprintf(stderr,"Done - exiting/n");
return 0;
}
Unix编程常见问题解答《6 工具的使用》
6. 工具的使用
*************
6.1 我怎样调试fork函数产生的子进程?
====================================
根据可用的工具有两种不同的方法:
你的调试器(debugger)可能有允许你选择是否跟踪调用‘fork()’以后的父或子进程
的选项,对于某些目的来说那已经足够了。
替换方法是,你的调试器可能有一个选项允许你将它依附(attach)到一个正在执行
的程序。这样你可以依附调试器到一个已经开始执行的子进程。如果你不需要从
子进程一开始就开始测试,这通常已经足够。否则,你会希望在子进程的‘fork()’
调用后插入一个‘sleep()’调用,或者插入如下的循环:
{
volatile int f = 1;
while(f);
}
这样子进程将一直在此循环不往下执行直到你用调试器设定‘f’为0。
并且记住,使用调试器并非是找到你程序中错误的唯一方法;在很多Unix系统
上有一些工具程序可用来跟踪系统调用和信号,而且丰富的日志经常也是有
用的。
6.2 怎样通过其他库文件建立新的库文件?
======================================
前提是我们所说的是归档(archive)(静态)库,最简单的方法是将所有选择的库
文件使用‘ar x’命令在一个空目录里拆分成它们原始的单个目标文件(object),
然后再合并成一个。当然,文件名有可能会重复,但是如果这个库文件很大,
你也许一开始就不想将它们合并在一起。
6.3 怎样创建动态连接库(shared library)/dlls?
=============================================
创建动态连接库(shared libraries)的方法根据不同的系统有所不同。这个过程主要
分两步;第一步要求包括在动态连接库中的目标必须首先是编译好的,通常需
要某个编译选项指示这串代码是位置无关的(position-indepenent);第二步,是将
这些目标连接在一起形成一个库文件。
这里是一个演示以上道理的小程序:
/* shrobj.c 文件 */
const char *myfunc()
{
return "Hello World";
}
/* shrobj.c 结束 */
/* hello.c 文件 */
#include
extern const char *myfunc();
main()
{
printf("%s/n", myfunc());
return 0;
}
/* hello.c 结束 */
$ gcc -fpic -c shrobj.c
$ gcc -shared -o libshared.so shrobj.o
$ gcc hello.c libshared.so
$ ./a.out
Hello World
到目前为止,如果你希望库文件和它的创建过程是都可以移植的话,那么最好
的办法是使用GNU Libtool程序包。它是个小型的工具程序套件,这些工具程序
知道建立动态连接库的平台无关性;你可以只发布你的程序必要的部分,从而
当一个安装者配置你的软件包时,他能决定生成什么库。Libtool程序包在不支持
动态连接库的系统上也能工作得很好。它而且知道与GNU Autoconf程序和GNU
Automake程序挂钩(如果你使用这些工具来管理你程序的编译创建过程)。
如果你不想使用Libtool程序包,那么对于gcc以外的编译器,你需要按照下面所
列修改编译器参数:
AIX 3.2 使用 xlc (未证实)
取消‘-fpic’选项,以‘-bM:SRE -bE:libshared.exp’取代‘-shared’。你并且
需要创建一个名为‘libshared.exp’的文件保存一个所有输出符号(symbols to export)
的列表,比如以上的范例程序,就需要输出‘myfunc’符号。另外,在连接库
时使用‘-e _nostart’参数(在较新的AIX版本上,我相信应该将其变成‘-bnoentry’)。
SCO OpenServer 5 使用 SCO 开发系统(Development System) (未证实)
如果你使用ELF(译者注:ELF:执行与连接格式Executable and Linking Forrmat,
一种Unix可执行目标文件的格式)格式,那么共享库只能在OS5上可用,而它
需要‘-belf’选项。并以‘-Kpic’取代‘-fpic’,在连接时使用‘cc -belf -G’。
Solaris 使用 SparcWorks 编译器
以‘-pic’取代‘-fpic’,并以‘ld -G’取代‘gcc -shared’。
(鼓励大家提供更多的材料丰富上述列表)
其它要当心的问题:
* AIX和(我相信)Digital Unix不需要-fpic选项,因为所有代码都是位置无关的。
* AIX一般需要你创建一个‘输出文件’,即一个保存所有动态连接库中输出
符号的列表。一些连接器(linker)版本(可能只有SLHS连接器,是svld?)有一个
选项可以输出所有符号。
* 如果你对于连接器想使用普遍的‘-l’参数来引用你的动态连接库,你必须
理解你的系统在实际运行时是怎样寻找动态连接库的。最普通的方法是使用
‘LD_LIBRARY_PATH’环境变量,但是通常在连接时有一种其它选项可以
设定。
* 大多数实现方法是在程序内部记录所希望的动态连接库在运行时的位置。这
样把一个动态连接库从一个目录移到另一个目录将导致程序无法工作。许多
系统对于连接器有一个选项用以设定希望运行时动态连接库的位置(比如在
Solaris系统上是‘-R’连接器选项,或者是‘LD_RUN_PATH’环境变量)。
* ELF和a.out的实现方法可能有一个连接器选项‘-Bsymbolic’,它导致在库本
身内部的引用被解释好。否则,在这些系统上,所有符号的解释将推迟到最
后连接时再进行,这样在main程序中的单一函数将重载库中的对应函数。
6.4 我能更改一个动态连接库里的目标吗?
======================================
一般不行。
在大多数系统上(除了AIX),当你连接目标并形成一个动态连接库时,它就象连
接一个可执行程序;目标并不保留它们单一的特征。结果是,一般不能从一个动
态连接库里析取出或更换一个单一的目标。
6.5 我能在一个运行着的程序中生成堆栈映象吗?
============================================
一些系统提供库函数可以提取(unwinding)出堆栈,从而(比如)你可以在一个错误
处理函数中生成一个堆栈映象,但是只有一小部分系统有这些函数。
一个可能的变通方法(workaround)是让你的程序执行一个调试器调试*它自己* -
详细方法仍然根据不同系统稍有不同,但一般的概念是这样:
void dump_stack(void)
{
char s[160];
sprintf(s, "/bin/echo 'where/ndetach' | dbx -a %d", getpid());
system(s);
return;
}
你需要根据你不同的系统对dbx的参数和命令进行加工,或者甚至换另一个调试
器,比如‘gdb’,但这仍然是我见过的对于这种问题最普遍的解决方法。为此,
荣誉授予Ralph Corderoy。
下面列表包含在一些系统上需要用到的命令行:
大多数使用dbx的系统
`"/bin/echo 'where/ndetach' | dbx /path/to/program %d"'
AIX
`"/bin/echo 'where/ndetach' | dbx -a %d"'
IRIX
`"/bin/echo 'where/ndetach' | dbx -p %d"'
?
范例程序
********
捕获 SIGCHLD 信号
=================
#include /* 在任何其它 sys 下的头文件之前引用这个头文件 */
#include /* waitpid()和一些不同的宏所需的头文件 */
#include /* 信号函数的头文件 */
#include /* fprintf函数的头文件 */
#include /* fork函数的头文件 */
void sig_chld(int); /* 我们的 SIGCHLD 信号处理函数的原形(prototype) */
int main()
{
struct sigaction act;
pid_t pid;
/* 设定sig_chld函数作为我们SIGCHLD信号的处理函数 */
act.sa_handler = sig_chld;
/* 在这个范例程序里,我们不想阻塞其它信号 */
sigemptyset(&act.sa_mask);
/*
* 我们只关谋恢罩沟淖咏蹋皇潜恢卸? * 的子进程 (比如用户在终端上按Control-Z)
*/
act.sa_flags = SA_NOCLDSTOP;
/*
* 使这些设定的值生效. 如果我们是写一个真实的应用程序,
* 我们也许应该保存这些原有值,而不是传递一个NULL。
*/
if (sigaction(SIGCHLD, &act, NULL) < 0)
{
fprintf(stderr, "sigaction failed/n");
return 1;
}
/* fork */
switch (pid = fork())
{
case -1:
fprintf(stderr, "fork failed/n");
return 1;
case 0: /* 是子进程,直接结束 */
_exit(7); /* 退出状态 = 7 */
default: /* 父进程 */
sleep(10); /* 给子进程完成的时间 */
}
return 0;
}
/*
* 信号处理函数 -- 只有当接收到一个SIGCHLD信号才被调用,
* 即有一个子进程终止
*/
void sig_chld(int signo)
{
int status, child_val;
/* 非阻塞地等待任何子进程结束 */
if (waitpid(-1, &status, WNOHANG) < 0)
{
/*
* 不建议在信号处理函数中调用标准输入/输出函数,
* 但在一个类似这个的玩具程序里或许没问题
*/
fprintf(stderr, "waitpid failed/n");
return;
}
/*
* 我们现在有保存在‘status’变量中的子进程退出信息并可以使用
* wait.h中定义的宏对其进行操作
*/
if (WIFEXITED(status)) /* 子进程是正常退出吗? */
{
child_val = WEXITSTATUS(status); /* 获取子进程的退出状态 */
printf("child's exited normally with status %d/n", child_val);
}
}
读取进程表 - SUNOS 4 版
=======================
#define _KMEMUSER
#include
#include
#include
char regexpstr[256];
#define INIT register char *sp=regexpstr;
#define GETC() (*sp++)
#define PEEKC() (*sp)
#define UNGETC(c) (--sp)
#define RETURN(pointer) return(pointer);
#define ERROR(val)
#include
pid_t
getpidbyname(char *name,pid_t skipit)
{
kvm_t *kd;
char **arg;
int error;
char *p_name=NULL;
char expbuf[256];
char **freeme;
int curpid;
struct user * cur_user;
struct user myuser;
struct proc * cur_proc;
if((kd=kvm_open(NULL,NULL,NULL,O_RDONLY,NULL))==NULL){
return(-1);
}
sprintf(regexpstr,"^.*/%s$",name);
compile(NULL,expbuf,expbuf+256,'/0');
while(cur_proc=kvm_nextproc(kd)){
curpid = cur_proc->p_pid;
if((cur_user=kvm_getu(kd,cur_proc))!=NULL){
error=kvm_getcmd(kd,cur_proc,cur_user,&arg,NULL);
if(error==-1){
if(cur_user->u_comm[0]!='/0'){
p_name=cur_user->u_comm;
}
}
else{
p_name=arg[0];
}
}
if(p_name){
if(!strcmp(p_name,name)){
if(error!=-1){
free(arg);
}
if(skipit!=-1 && ourretval==skipit){
ourretval=-1;
}
else{
close(fd);
break;
}
break;
}
else{
if(step(p_name,expbuf)){
if(error!=-1){
free(arg);
}
break;
}
}
}
if(error!=-1){
free(arg);
}
p_name=NULL;
}
kvm_close(kd);
if(p_name!=NULL){
return(curpid);
}
return (-1);
}
读取进程表 - SYSV 版
====================
pid_t
getpidbyname(char *name,pid_t skipit)
{
DIR *dp;
struct dirent *dirp;
prpsinfo_t retval;
int fd;
pid_t ourretval=-1;
if((dp=opendir("/proc"))==NULL){
return -1;
}
chdir("/proc");
while((dirp=readdir(dp))!=NULL){
if(dirp->d_name[0]!='.'){
if((fd=open(dirp->d_name,O_RDONLY))!=-1){
if(ioctl(fd,PIOCPSINFO,&retval)!=-1){
if(!strcmp(retval.pr_fname,name)){
ourretval=(pid_t)atoi(dirp->d_name);
if(skipit!=-1 && ourretval==skipit){
ourretval=-1;
}
else{
close(fd);
break;
}
}
}
close(fd);
}
}
}
closedir(dp);
return ourretval;
}
读取进程表 - AIX 4.2 版
=======================
#include
#include
int getprocs(struct procsinfo *, int, struct fdsinfo *,
int, pid_t *, int);
pid_t getpidbyname(char *name, pid_t *nextPid)
{
struct procsinfo pi;
pid_t retval = (pid_t) -1;
pid_t pid;
pid = *nextPid;
while(1)
{
if(getprocs(&pi, sizeof pi, 0, 0, &pid, 1) != 1)
break;
if(!strcmp(name, pi.pi_comm))
{
retval = pi.pi_pid;
*nextPid = pid;
break;
}
}
return retval;
}
int main(int argc, char *argv[])
{
int curArg;
pid_t pid;
pid_t nextPid;
if(argc == 1)
{
printf("syntax: %s [program ...]/n",argv[0]);
exit(1);
}
for(curArg = 1; curArg < argc; curArg++)
{
printf("Process IDs for %s/n", argv[curArg]);
for(nextPid = 0, pid = 0; pid != -1; )
if((pid = getpidbyname(argv[curArg], &nextPid)) != -1)
printf("/t%d/n", pid);
}
}
使用popen函数和ps命令读取进程表
===============================
#include /* FILE, sprintf, fgets, puts */
#include /* atoi, exit, EXIT_SUCCESS */
#include /* strtok, strcmp */
#include /* pid_t */
#include /* WIFEXITED, WEXITSTATUS */
char *procname(pid_t pid)
{
static char line[133], command[80], *linep, *token, *cmd;
FILE *fp;
int status;
if (0 == pid) return (char *)0;
sprintf(command, "ps -p %d 2>/dev/null", pid);
fp = popen(command, "r");
if ((FILE *)0 == fp) return (char *)0;
/* 读取标题行 */
if ((char *)0 == fgets(line, sizeof line, fp))
{
pclose(fp);
return (char *)0;
}
/* 从标题栏分析出命令名所在列。
* (BSD风格的系统将指示命令的"COMMAND"字符串放在第5列,SysV好象将
* 指示命令的“CMD”或“COMMAND”字符串放在第4列)
*/
for (linep = line; ; linep = (char *)0)
{
if ((char *)0 == (token = strtok(linep, " /t/n")))
{
pclose(fp);
return (char *)0;
}
if (0 == strcmp("COMMAND", token) || 0 == strcmp("CMD", token))
{ /* 我们找到COMMAND所在列 */
cmd = token;
break;
}
}
/* 读取 ps(1) 输出行 */
if ((char *)0 == fgets(line, sizeof line, fp))
{
pclose(fp);
return (char *)0;
}
/* 抓COMMAND标题下面的词 ... */
if ((char *)0 == (token = strtok(cmd, " /t/n")))
{
pclose(fp);
return (char *)0;
}
status = pclose(fp);
if (!WIFEXITED(status) || 0 != WEXITSTATUS(status))
return (char *)0;
return token;
}
int main(int argc, char *argv[])
{
puts(procname(atoi(argv[1])));
exit(EXIT_SUCCESS);
}
守护程序工具函数
================
#include
#include
#include
#include
#include
#include
#include
/* closeall() -- 关闭所有>=给定值的文件描述符 */
void closeall(int fd)
{
int fdlimit = sysconf(_SC_OPEN_MAX);
while (fd < fdlimit)
close(fd++);
}
/* daemon() - 将进程从用户端脱离并消失进入后台,若失败返回-1,
* 但是在那种情况下你只能退出,因为我们可能已经生成了子进程。
* 这是基于BSD的版本,所以调用方需负责类似umask等等其它的工作。
*/
/* 相信在所有Posix系统上都能工作 */
int daemon(int nochdir, int noclose)
{
switch (fork())
{
case 0: break;
case -1: return -1;
default: _exit(0); /* 原进程退出 */
}
if (setsid() < 0) /* 不应该失败 */
return -1;
/* 如果你希望将来获得一个控制tty,则排除(dyke)以下的switch语句 */
/* -- 正常情况不建议用于守护程序 */
switch (fork())
{
case 0: break;
case -1: return -1;
default: _exit(0);
}
if (!nochdir)
chdir("/");
if (!noclose)
{
closeall(0);
open("/dev/null",O_RDWR);
dup(0); dup(0);
}
return 0;
}
/* fork2() -- 类似fork函数,但子进程立刻变成孤儿进程
* (当它退出时不产生僵死进程)
* 返回1给父进程,不是任何有意义的进程号.
* 父进程不能使用wait函数等待子进程结束 (它们是无关的).
*/
/* 这个版本假设你没有捕获和忽略SIGCHLD信号. */
/* 如果你有设定,则不管怎样应使用fork函数 */
int fork2()
{
pid_t pid;
int rc;
int status;
if (!(pid = fork()))
{
switch (fork())
{
case 0: return 0;
case -1: _exit(errno); /* 假设错误码都小于256 */
default: _exit(0);
}
}
if (pid < 0 || waitpid(pid,&status,0) < 0)
return -1;
if (WIFEXITED(status))
if (WEXITSTATUS(status) == 0)
return 1;
else
errno = WEXITSTATUS(status);
else
errno = EINTR; /* 唉,类似这个 :-) */
return -1;
}
一个使用以上函数的范例程序:
#include
#include
#include
#include
#include
#include
#include
int daemon(int,int);
int fork2(void);
void closeall(int);
#define TCP_PORT 8888
void errexit(const char *str)
{
syslog(LOG_INFO, "%s failed: %d (%m)", str, errno);
exit(1);
}
void errreport(const char *str)
{
syslog(LOG_INFO, "%s failed: %d (%m)", str, errno);
}
/* 实际的子进程在此. */
void run_child(int sock)
{
FILE *in = fdopen(sock,"r");
FILE *out = fdopen(sock,"w");
int ch;
setvbuf(in, NULL, _IOFBF, 1024);
setvbuf(out, NULL, _IOLBF, 1024);
while ((ch = fgetc(in)) != EOF)
fputc(toupper(ch), out);
fclose(out);
}
/* 这是守护程序的主要工作 -- 侦听连接并生成子进程 */
void process()
{
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int sock = socket(AF_INET, SOCK_STREAM, 0);
int flag = 1;
int rc = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
&flag, sizeof(flag));
if (rc < 0)
errexit("setsockopt");
addr.sin_family = AF_INET;
addr.sin_port = htons(TCP_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
rc = bind(sock, (struct sockaddr *) &addr, addrlen);
if (rc < 0)
errexit("bind");
rc = listen(sock, 5);
if (rc < 0)
errexit("listen");
for (;;)
{
rc = accept(sock, (struct sockaddr *) &addr, &addrlen);
if (rc >= 0)
switch (fork2())
{
case 0: close(sock); run_child(rc); _exit(0);
case -1: errreport("fork2"); close(rc); break;
default: close(rc);
}
}
}
int main()
{
if (daemon(0,0) < 0)
{
perror("daemon");
exit(2);
}
openlog("test", LOG_PID, LOG_DAEMON);
process();
return 0;
}
调制解调器控制范例程序
======================
/* 发出一些简单调制解调器命令
* 需要串行设备的设备名 (最好是拨出设备,
* 或者是非调制解调器控制设备) 作为它唯一的参数.
* 如果你没有可共使用的拨出设备, 那么以CFLAGS_TO_SET取代CLOCAL。
*/
#include
#include
#include
#include
#include
#include
#include /* 也许需要;和系统有关 */
#include
#include
#include
#include
#define CFLAGS_TO_SET (CREAD | HUPCL)
#define CFLAGS_TO_CLEAR (CSTOPB | PARENB | CLOCAL)
enum flowmode { NoFlow, HardFlow, SoftFlow };
/* 和系统有关 */
#define CFLAGS_HARDFLOW (CRTSCTS)
#define EXAMPLE_BAUD B19200
#define EXAMPLE_FLOW HardFlow
static void die(const char *msg)
{
fprintf(stderr, "%s/n", msg);
exit(1);
}
static int close_and_complain(int fd, const char *msg, int err)
{
fprintf(stderr, "%s: %s/n", msg, strerror(err));
if (fd >= 0)
close(fd);
errno = err;
return -1;
}
int open_port(const char *name, speed_t baud, enum flowmode flow)
{
int flags;
struct termios attr;
int fd = open(name, O_RDWR | O_NONBLOCK | O_NOCTTY);
if (fd < 0)
return close_and_complain(-1, "open", errno);
/* 设定一些不明确是否敏感的值 */
if (tcgetattr(fd, &attr) < 0)
return close_and_complain(fd, "tcgetattr", errno);
/* 无特殊输入或输出处理 */
attr.c_iflag = (flow == SoftFlow) ? (IXON | IXOFF) : 0;
attr.c_oflag = 0;
/* 设定8位字符宽和一些杂项控制模式 */
attr.c_cflag &= ~(CSIZE | CFLAGS_TO_CLEAR | CFLAGS_HARDFLOW);
attr.c_cflag |= (CS8 | CFLAGS_TO_SET);
if (flow == HardFlow)
attr.c_cflag |= CFLAGS_HARDFLOW;
/* 本机模式 */
attr.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ISIG);
/* 特殊字符 -- 许多已被先前的设定取消 */
{
int i;
#ifdef _POSIX_VDISABLE
attr.c_cc[0] = _POSIX_VDISABLE;
#else
attr.c_cc[0] = fpathconf(fd, _PC_VDISABLE);
#endif
for (i = 1; i < NCCS; i++)
attr.c_cc[i] = attr.c_cc[0];
}
attr.c_cc[VSTART] = 0x11;
attr.c_cc[VSTOP] = 0x13;
/* 对read()函数的计时控制 */
attr.c_cc[VMIN] = 1;
attr.c_cc[VTIME] = 0;
/* 波特律 */
cfsetispeed(&attr, baud);
cfsetospeed(&attr, baud);
/* 写入设定 */
if (tcsetattr(fd, TCSANOW, &attr) < 0)
return close_and_complain(fd, "tcsetattr", errno);
/* 如果系统记住了先前的O_NONBLOCK设定,就取消它 */
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
return close_and_complain(fd, "fcntl(GETFL)", errno);
if (fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0)
return close_and_complain(fd, "fcntl(SETFL)", errno);
return fd;
}
/* 一些简单的计时工具函数 */
/* 向*TV加 SECS 和USECS */
static void timeradd(struct timeval *tv, long secs, long usecs)
{
tv->tv_sec += secs;
if ((tv->tv_usec += usecs) >= 1000000)
{
tv->tv_sec += tv->tv_usec / 1000000;
tv->tv_usec %= 1000000;
}
}
/* 设定 *RES = *A - *B, 返回结果的符号 */
static int timersub(struct timeval *res,
const struct timeval *a, const struct timeval *b)
{
long sec = a->tv_sec - b->tv_sec;
long usec = a->tv_usec - b->tv_usec;
if (usec < 0)
usec += 1000000, --sec;
res->tv_sec = sec;
res->tv_usec = usec;
return (sec < 0) ? (-1) : ((sec == 0 && usec == 0) ? 0 : 1);
}
/* 这个函数不试图处理非正常的字符串 (比如 ababc)
* 超时以微妙计
* 一个更通常的做法是使用alarm()函数处理超时.
* 这个函数为简便起见不使用信号处理并试图提供一种替换方法
*/
int expect(int fd, const char *str, int timeo)
{
int matchlen = 0;
int len = strlen(str);
struct timeval now,end,left;
fd_set fds;
char c;
gettimeofday(&end, NULL);
timeradd(&end, timeo/1000, timeo%1000);
while (matchlen < len)
{
gettimeofday(&now, NULL);
if (timersub(&left, &end, &now) <= 0)
return -1;
FD_ZERO(&fds);
FD_SET(fd, &fds);
if (select(fd+1, &fds, NULL, NULL, &left) <= 0)
return -1;
if (read(fd, &c, 1) != 1)
return -1;
if (isprint((unsigned char)c) || c == '/n' || c == '/r')
putchar(c);
else
printf("//x%02x", c);
if (c == str[matchlen])
++matchlen;
else
matchlen = 0;
}
return 0;
}
int main(int argc, char **argv)
{
int fd;
unsigned char c;
if (argc < 2)
die("no port specified");
setvbuf(stdout, NULL, _IONBF, 0);
fd = open_port(argv[1], EXAMPLE_BAUD, EXAMPLE_FLOW);
if (fd < 0)
die("cannot open port");
write(fd, "AT/r", 3);
if (expect(fd, "OK", 5000) < 0)
{
write(fd, "AT/r", 3);
if (expect(fd, "OK", 5000) < 0)
{
tcflush(fd, TCIOFLUSH);
close(fd);
die("no response to AT");
}
}
write(fd, "ATI4/r", 5);
expect(fd, "OK", 10000);
putchar('/n');
tcflush(fd, TCIOFLUSH);
close(fd);
return 0;
}
事务控制范例程序
================
/* 生成前台/后台事务的函数 */
#include
#include
#include
#include
#include
#include
#include
#include
/* 一些下面的函数会因为无法定位控制tty和调用方不在前台而失败。
* 第一种情况时,我们假设一个前台程序会有为标准输入,标准输出或标准错误输出打开的ctty,
* 而如果没有则返回ENOTTY。
* 第二种情况时,除foreground_self()函数的特殊情况以外,
* 若一个非前台程序打算输出一些东西到前台,我们返回EPERM。
* (也许想得太多了)
*/
/* 为给定的pgrp安排一个终端 (打开一个ctty) .
* 这个tcsetpgrp()外壳程序只是因为POSIX中特别错误(bogusity)的地方而需要;
* 遵照标准的系统在一个非前台进程调用tcsetpgrp函数时传递SIGTTOU
* 信号(差不多总是这样)。这是虚假的一致性之于一般想法的胜利。
*/
int assign_terminal(int ctty, pid_t pgrp)
{
sigset_t sigs;
sigset_t oldsigs;
int rc;
sigemptyset(&sigs);
sigaddset(&sigs,SIGTTOU);
sigprocmask(SIG_BLOCK, &sigs, &oldsigs);
rc = tcsetpgrp(ctty, pgrp);
sigprocmask(SIG_SETMASK, &oldsigs, NULL);
return rc;
}
/* 类似fork函数,但做事务控制。如果新建立的进程放在前台则设fg为真。
* (这样隐式地将调用方进程放置到后台,所以做完这个后要当心tty的输入/输出)
* 设定pgrp为-1以创建一个新事务,在此情况下返回的进程号即是新事务的进程组号,
* 或者设定一个同一会话中存在的事务(一般只用来启动管道操作的第二个或第二个以后
* 的进程)。
*/
pid_t spawn_job(int fg, pid_t pgrp)
{
int ctty = -1;
pid_t pid;
/* 如果生成一个*新*的前台事务,起码要求标准输入,标准输出或
* 标准错误输出的其中一个指向的是控制tty,并且当前进程在前台。
* 只有当在存在事务中开始一个新前台进程时才检查控制中的tty。
* 一个没有控制tty的会话只能有后台事务。
*/
if (fg)
{
pid_t curpgrp;
if ((curpgrp = tcgetpgrp(ctty = 2)) < 0
&& (curpgrp = tcgetpgrp(ctty = 0)) < 0
&& (curpgrp = tcgetpgrp(ctty = 1)) < 0)
return errno = ENOTTY, (pid_t)-1;
if (pgrp < 0 && curpgrp != getpgrp())
return errno = EPERM, (pid_t)-1;
}
switch (pid = fork())
{
case -1: /* fork失败 */
return pid;
case 0: /* 子进程 */
/* 建立新进程组, 如果需要则将我们放到前台
* 不知道如果setpgid函数调用失败该怎么办(“不会发生”)
*/
if (pgrp < 0)
pgrp = getpid();
if (setpgid(0,pgrp) == 0 && fg)
assign_terminal(ctty, pgrp);
return 0;
default: /* 父进程 */
/* 这里也建立自进程组. */
if (pgrp < 0)
pgrp = pid;
setpgid(pid, pgrp);
return pid;
}
/*不会执行到这里*/
}
/* 用SIGNO表示的信号杀死PGRP表示的事务 */
int kill_job(pid_t pgrp, int signo)
{
return kill(-pgrp, signo);
}
/* 中断PGRP表示的事务 */
int suspend_job(pid_t pgrp)
{
return kill_job(pgrp, SIGSTOP);
}
/* 继续在后台执行PGRP表示的事务 */
int resume_job_bg(pid_t pgrp)
{
return kill_job(pgrp, SIGCONT);
}
/* 继续在前台执行PGRP表示的事务 */
int resume_job_fg(pid_t pgrp)
{
pid_t curpgrp;
int ctty;
if ((curpgrp = tcgetpgrp(ctty = 2)) < 0
&& (curpgrp = tcgetpgrp(ctty = 0)) < 0
&& (curpgrp = tcgetpgrp(ctty = 1)) < 0)
return errno = ENOTTY, (pid_t)-1;
if (curpgrp != getpgrp())
return errno = EPERM, (pid_t)-1;
if (assign_terminal(ctty, pgrp) < 0)
return -1;
return kill_job(pgrp, SIGCONT);
}
/* 将我们自己放置到前台,比如在中断一个前台事务之后调用
*/
int foreground_self()
{
pid_t curpgrp;
int ctty;
if ((curpgrp = tcgetpgrp(ctty = 2)) < 0
&& (curpgrp = tcgetpgrp(ctty = 0)) < 0
&& (curpgrp = tcgetpgrp(ctty = 1)) < 0)
return errno = ENOTTY, (pid_t)-1;
return assign_terminal(ctty, getpgrp());
}
/* closeall() - 关闭所有>=给定FD的文件描述符 */
void closeall(int fd)
{
int fdlimit = sysconf(_SC_OPEN_MAX);
while (fd < fdlimit)
close(fd++);
}
/* 类似system()函数,但将给定的命令作为后台事务执行,返回shell进程
* 的进程号(并且也是这个事务的进程组号,适用于kill_job等等)。
* 如果参数INFD,OUTFD或ERRFD为非NULL,则打开一个管道和一个文件描述
* 符保存与该管道有关的父进程端,然后在子进程中将被从定向到/dev/null。
* 并且在子进程中关闭所有>2的文件描述符(一个经常过份估计的工作)
*/
pid_t spawn_background_command(const char *cmd,
int *infd, int *outfd, int *errfd)
{
int nullfd = -1;
int pipefds[3][2];
int error = 0;
if (!cmd)
return errno = EINVAL, -1;
pipefds[0][0] = pipefds[0][1] = -1;
pipefds[1][0] = pipefds[1][1] = -1;
pipefds[2][0] = pipefds[2][1] = -1;
if (infd && pipe(pipefds[0]) < 0)
error = errno;
else if (outfd && pipe(pipefds[1]) < 0)
error = errno;
else if (errfd && pipe(pipefds[2]) < 0)
error = errno;
if (!error && !(infd && outfd && errfd))
{
nullfd = open("/dev/null",O_RDWR);
if (nullfd < 0)
error = errno;
}
if (!error)
{
pid_t pid = spawn_job(0, -1);
switch (pid)
{
case -1: /* fork失败 */
error = errno;
break;
case 0: /* 子进程 */
dup2(infd ? pipefds[0][0] : nullfd, 0);
dup2(outfd ? pipefds[1][1] : nullfd, 1);
dup2(errfd ? pipefds[2][1] : nullfd, 2);
closeall(3);
execl("/bin/sh","sh","-c",cmd,(char*)NULL);
_exit(127);
default: /* 父进程 */
close(nullfd);
if (infd)
close(pipefds[0][0]), *infd = pipefds[0][1];
if (outfd)
close(pipefds[1][1]), *outfd = pipefds[1][0];
if (errfd)
close(pipefds[2][1]), *errfd = pipefds[2][0];
return pid;
}
}
/* 只在错误时执行到这里 */
{
int i,j;
for (i = 0; i < 3; ++i)
for (j = 0; j < 2; ++j)
if (pipefds[i][j] >= 0)
close(pipefds[i][j]);
}
if (nullfd >= 0)
close(nullfd);
return errno = error, (pid_t) -1;
}
/*---------------------------------------*/
/* 这里是使用上述函数一个小例子. */
pid_t bgjob = -1;
volatile int signo = 0;
#ifndef WCOREDUMP
/* 如果没有 WCOREDUMP, 你也许会希望在你的平台上为它设置一个准确的定义
* (这通常是(status & 0x80) 但也不总是这样),或者就赌没有core dumps(
* 就象这个程序所做)
*/
# define WCOREDUMP(status) (0)
#endif
int check_children()
{
pid_t pid;
int status;
int count = 0;
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if (pid == bgjob && !WIFSTOPPED(status))
bgjob = -1;
++count;
if (WIFEXITED(status))
fprintf(stderr,"Process %ld exited with return code %d/n",
(long)pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
fprintf(stderr,"Process %ld killed by signal %d%s/n",
(long)pid, WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
else if (WIFSTOPPED(status))
fprintf(stderr,"Process %ld stopped by signal %d/n",
(long)pid, WSTOPSIG(status));
else
fprintf(stderr,"Unexpected status - pid=%ld, status=0x%x/n",
(long)pid, status);
}
return count;
}
void sighandler(int sig)
{
if (sig != SIGCHLD)
signo = sig;
}
int main()
{
struct sigaction act;
int sigcount = 0;
act.sa_handler = sighandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT,&act,NULL);
sigaction(SIGQUIT,&act,NULL);
sigaction(SIGTERM,&act,NULL);
sigaction(SIGTSTP,&act,NULL);
sigaction(SIGCHLD,&act,NULL);
fprintf(stderr,"Starting background job 'sleep 60'/n");
bgjob = spawn_background_command("sleep 60", NULL, NULL, NULL);
if (bgjob < 0)
{
perror("spawn_background_command");
exit(1);
}
fprintf(stderr,"Background job started with id %ld/n", (long)bgjob);
while (bgjob >= 0)
{
if (signo)
{
fprintf(stderr,"Signal %d caught/n", signo);
if (sigcount++)
kill_job(bgjob, SIGKILL);
else
{
kill_job(bgjob, SIGTERM);
kill_job(bgjob, SIGCONT);
}
}
if (!check_children())
pause();
}
fprintf(stderr,"Done - exiting/n");
return 0;
}