开源中文网

您的位置: 首页 > FreeBSD > 正文

freebsd系统编程[简体中文版]3

来源:  作者:

3.4 进程创建和进程ID系统 

就像上面所说,当一个进程被运行,内核指派给它一个唯一PID。这个PID是个正整数并且它的值在0到PID_MAX之间,由系统决定。(对于FreeBSD,在/usr/include/sys/proc.h中设定PID_MAX为99999。)内核将用下一个相续的可用的值来指派PID。因此,当PID_MAX到达的时候,值将会循环。这个循环是重要的,当使用PID于当前进程统计的时候。 
每一个在系统上运行的进程都是由其他进程产生的。这是有一些系统调用完成的,将在下一章论述。当一个进程创建一个新的进程,原进程就成为父进程,新进程就是子进程。这种父子关系提供了很好的类推-每一个父进程可以有很多子进程而父进程们是由另外一个进程派生的。进程可以通过getpid/getppid函数得到自己或他们父进程的PID。 

进程也可以用进程组来分组。这些进程组可以通过一个唯一的grpID来标志。进程组作为一个机制被引入BSD是为了使shells能够控制工作。看下面这个例子: 

bash___FCKpd___0nbsp;ps auwx  | grep root  | awk {'print $2' }


这些程序,ps,grep,awk和print都归属于一个相同的组,这允许所有的这些指令可以通过一个单一的工作来控制。 

一个进程可以获得它的组ID/和父ID通过调用getpgrp或getppid: 
  #include <sys/types.h>

  #include <unistd.h>

  

     pid_t   getpid(void);

     pid_t   getppid(void);

     pid_t   getpgrp(void);


所有以上例出的函数都使绝对可以运行成功的。然而,FreeBSD man pages 强烈建议,PID不应该用来创建一个唯一文件,因为这个PID只在创建的时候唯一。一旦进程退出,PID值会回到未使用PID池,将会被接着被使用(当然是在系统一直运行情况下)。 

一个获得这些值的源程序代码在 proc_ids.c中列出。 
http://www.khmere.com/freebsd_book/src/03/proc_ids.c.html 

当你运行下面程序: 
bash___FCKpd___2nbsp;sleep 10 |  ps auwx |  awk {'print $2'}  | ./proc_ids


同时在另一个终端运行: 
bash___FCKpd___3nbsp;ps -j



只有这些指令将被一个shell运行并且每个都有相同的PPID和PGID。 

3.5 子进程 

进程可以由其他进程创建,在BSD中有3个方法来实现这个目的。他们是:fork,vfork 和rfork 。其他的调用例如system不过是对这3个的封装。 

fork 

当一个进程调用frok,一个新的复制父进程的进程奖被创建: 

 #include <sys/types.h>

  #include <unistd.h>



     pid_t   fork(void);


不像其他的函数调用,在成功的时候,fork会反馈两次-一次在父进程,返回的是新创建的子进程的PID,而第二次在子进程,返回将是0。这样一来,你就可以区分两个进程。当一个新的进程被fork创建,它几乎是父进程的精确复制。他们的共同点包括一下:(不是创建的顺序) 

Controlling terminal  
Working directory  
Root directory  
Nice value  
Resource limits  
Real, effective and saved user ID  
Real, effective and saved group ID  
Process group ID  
Supplementary group ID  
The set-user-id bits  
The set-group-id bits  
All saved environment variables  
All open file descriptors and current offsets  
Close-on-exec flags  
File mode creation (umask)  
Signals handling settings (SIG_DFL, SIG_IGN, addresses)  
Signals mask  

在子进程中不同的是它的PID,而PPID被设置成父进程的PID,进程的资源利用值是0,并且拷贝了父进程的文件描述符。子进程可以关闭文件描述符而不干扰父进程。如果子进程希望读写它们,将会保持父进程的偏移量。注意一个潜在的问题,如果父子进程同时试图读写相同的文件描述符会导致输出异常,或程序崩溃。 

当一个新的进程被创建后,运行的顺序将不可知。最好控制运行顺序的方法是使用semiphores,pipes,或signals,这以后论述。通过这些,读写就可以控制,使得一个进程可以避免扰乱其他进程导致一起崩溃。 

wait 

父进程应该使用下面wait系统调用的一种来搜集子进程的退出状态: 

    #include <sys/types.h>

    #include <sys/wait.h>



     pid_t     wait(int *status);





     #include <sys/time.h>

     #include <sys/resource.h>



     pid_t     waitpid(pid_t wpid, int *status, int options);

     pid_t     wait3(int *status, int options, struct rusage *rusage);

     pid_t     wait4(pid_t  wpid, int *status, int options, struct rusage *rusage);





在调用wait的时候,options参数是一个位值,或者是以下的一个: 

WNOHANG -在调用的时候不阻塞程序,他会导致wait马上反馈即使没有子进程被终止。 

WUNTRACED -当你想要等待停止并且不可跟踪的子进程(可能由SIGTTIN, SIGTTOU, SIGTSTP或SIGSTOP信号导致)的状态时设置该值。 

WLINUXCLONE -如果你想等待通过linux_clone产生的内核线程。 

当使用wait4和waitpid调用时,要注意wpid参数是要等待的PID值。如果将其设置成-1将会导致该调用等待所有可能的子进程的终止。在调用时使用rusage结构,如果结构不是指向NULL将返回终止进程的资源使用统计。当然如果进程已经停止,那么这些使用信息就没有什么用处。 

wait函数调用提供给父进程一个方法去获取它子进程退出的信息。一当调用,调用它的进程将被阻塞知道一个子进程终止或接收到一个信号。这可以通过设置WNOHANG 值来避免。当调用成功时,status参数将包含结束进程的信息。如果调用进程对退出状态没有兴趣,可以将status设置成NULL。关于使用WNOHANG 的更多细节将在信号章节描述。 

如果对退出状态信息有兴趣,它们的宏定义在/usr/include/sys/wait.h中。最好使用它们来获得更好的跨平台兼容性。下面列出它们3个和使用说明: 

WIFEXITED(status) -如果返回true(也就是返回非0值)那么进程是通过调用exit()或_exit()正常终止的。 

WIFSIGNALED(status) -如果返回true那么进程是由信号终止的。 

WIFSTOPPED(status) -如果返回true那么进程已经停止并且可以重新开始。这个宏应该和WUNTRACED一起使用,或当子进程被跟踪的时候(就像使用ptrace) 

如果有必要,以下的宏可以进一步提取保存的状态中信息 

WEXITSTATUS(status) - 这只能在WIFEXITED(status) 宏评估为true时使用,他会评估exit()和_exit()传递参数的低8位。 

WTERMSIG(status) -这只能在WIFSIGNALED(status)评估为true时使用,他会得到导致进程终止的信号的值。 

WCOREDUMP(status) -这只能在WIFSIGNALED(status)评估为true时使用,如果终止的进程在收到信号的点创建了core dump文件那么该宏会返回true。 

WSTOPSIG(status) -这只能在WIFSTOPPED(status) 评估为true时使用。该红会得到导致进程停止的信号。 

如果父进程没有收集子进程的退出状态,或父进程在子进程结束前就已结束,init将会接管子进程并收集它们的退出状态。 

vfork 和 rfork 

vfork函数和fork相似,是在2.9BSD被引入的。它们两者的区别是,vfork会停止父进程的运行并且使用父进程的运行线程。这是为了调用execv函数设计的,这样可以避免低效的复制父进程的地址空间。实际上vfork是不被建议使用的,因为它不是跨平台的。例如Irix的5.x就没有vfork.下面是vfork的调用例子: 

 #include <sys/types.h>

 #include <unistd.h>

  

    int  vfork(void);



rfork函数也和fork与vfork相似.他是在Plan9引入的。它的主要目的是增加更成熟的方法来控制进程的创建和创建内核线程。FreeBSD/OpenBSD支持伪线程和Linux clone调用。换句话说,rfork允许比fork更快更小的进程创建。这个调用可以设置子进程可以和它们共享的资源。下面是一个rfork调用例程: 

  #include <unistd.h>



    int    rfork(int flags);



rfork可以选择的资源如下: 

RFPROC -设置该值表示你想创建一个新的进程;而其他的标志将只影响当前进程。该值是默认值。 

RFNOWAIT -设置该值表示你希望创建一个和父进程分离的子进程。一当该子进程被退出,他不会留下状态等待父进程收集。 

RFFDG -设置该值表示你希望复制父进程的文件描述符表。否则父子进程将会共享一个文件描述符表。 

RFCFDG  -该标志和RFDG标志互斥。设置该值表示你希望子进程拥有一个干净的文件描述符表。 

RFMEM -设置该值表示你想强制内核共享完整的地址空间。这是直接共享硬件页面表的典型做法。这不能直接在C中调用,因为子进程会返回和父进程相同堆栈。如果你想这么做,最好使用下面列出的rfork_thread函数: 

#include <unistd.h>



   int     rfork_thread(int flags, void *stack, int (*func)(void*arg), void *arg);



这将创建一个新的进程运行于指定的堆栈,并且调用参数指定的函数。和fork不同,成功的时候只会返回新创建的进程PID给父进程,因为子进程将直接运行指定的函数。如果调用失败将返回-1,并且设置errno的值。 

RFSIGSHARE -这是一个FreeBSD特有的标志并且最近众所周知地被用于FreeBSD4.3的缓冲区溢出。该值将允许子进程和父进程共享信号。(通过共享sigacts结构)。 

RFLINUXTHPN -这是另一个FreeBSD特有的值。这将导致内核在子进程退出时使用信号SIGUSR1代替SIGCHILD。我们会在下一章论述它,现在可以把它看作rfork模仿linux clone调用。 

rfork的返回值和fork相似。子进程获得0值而父进程获得子进程的PID。一个微小的区别是-rfork会在需要时休眠直到必要的系统资源可用。而一个fork调用和使用RFFDG | RFPROC来调用rfork相似。但是他不设计向后兼容。如果rfork调用失败会返回-1并且设置errno。(更多的error 标号信息可以看man page和头部文件。) 

显然,rfork调用提供了一个较小消耗的fork版本。缺点是有一些众所周知的安全漏洞。并且他即使在跨BSD系列平台上也不是很兼容,在不同的Unix版本中是独一无二的。比如当前的NetBSD-1.5就不支持rfork,而且不是所有FreeBSD可用的标志都可以用于OpenBSD。正因为如此,推荐的线程接口是使用pthreads。 

3.6 运行二进制程序 

一个进程如果仅仅是父进程的拷贝是没有很大用途的。因此,需要使用exec函数。这些是设计来使用新的进程镜像代替当前进程的镜像。好,举个例子,shell运行ls指令。首先shell会按照执行运行fork或vfork接着它会调用exec函数。一旦exec被成功调用一个新的进程将由ls代替运行,并且exec自己没有返回。如果像shell或perl这样的脚本是目标程序,这个过程就像二进制程序运行。那里有一个附加的程序调用解析器。那就是,头两个字符将是#! 例如,下面展示了一个带参数的解析器调用: 
#!  interpreter [arg]




下面的指令将使用-w参数调用Perl解析器: 

#!/usr/bin/perl  -w



下面列出了所有的exec调用。源代码 exec.c http://www.khmere.com/freebsd_book/src/03/exec.c.html 
调用它们的基本结构是(目标程序),(参数),(必要的环境变量)。 

  #include <sys/types.h>

  #include <unistd.h>

    

    extern char **environ;

    

    int     execl(const char *path, const char *arg0, ...  const char *argn,   NULL);

    int     execle(const char *path, const char *arg0, ... const char  *argn, NULL, char *const envp[]);

    int     execlp(const char *file, const char *arg0, ... const char *argn , NULL );

    int     exect(const char *path, char *const argv[], char *const envp[]);

    int     execv(const char *path, char *const argv[]);

    int     execvp(const char *file, char *const argv[]);



注意;在使用带有参数arg的exec时,arg参数必须是以NULL截止的字符串,就如在arg0,arg1,arg2 ..argn中必须以NULL或0结尾。如果没有确定结束符调用会失败。这字符串将成为目标程序运行的参数。 

调用exec使用*argv[]参数时其必须是一组包含结束符的字符串的数组,它们将成为目标程序运行的参数。 

另一个区分二进制或脚本运行exec的是目标程序是如何指定的。如果目标没有带路径,函数execlp和execvp会搜寻你的环境目录来查找目标程序。而其他函数调用将要求绝对路径。 

简索: 

数据参数和序列参数 

array: exect, execv, execvp  

sequence: execl, execle, eseclp  

路径搜索和直接文件 

path: execl, execle, exect, execv  

file: execlp, exevp  

指定环境和继承环境 

specify: execle, exect  

inherit: execl, execlp, execv, execvp  

system 

    

   #include <stdlib.h>



     int     system(const char *string);



另外一个关键的运行函数调用是system调用。这个函数十分直观。提供的参数将直接传送给shell。如果指定的参数 
为NULL,如果shell可用函数会返回1否则返回0。一旦调用,进城会等待shell结束并返回shell的结束状态。如果返回-1表示fork或waitpid失败,127表示shell运行失败。 

一旦调用,一些信号例如SIGINT和SIGQUIT将被忽略,并且会阻塞SIGCHILD。同样如果子进程写输出到标准错误输出可能会使调用进程崩溃。 

显然,BSD结构提供了简单同时丰富的进程创建接口。这种成熟的设计可以和任何一个现在操作系统媲美。下一章将包括信号和进程管理,包含资源使用,线程和进程限制。 


[size=3][align=center]第四章 高级进程控制和信号[/align][/size] 
[align=center]翻译: gvim@chinaunix/bsd[/align] 
引用:一些说明: 
1 很高兴有这个机会参与到这项活动中来。 
2 由于英语语言的习惯,文中有不少复杂长句,按照字面翻译出来并不适合中国人的阅读习惯。我在保留文章原意的基础上作了一些语言组织上的调整,将大部分长句子组织成中文擅长的短句型,所以你会发现翻译品一部分内容不是按照原文字面翻译的。 
3 考虑到该书的入门引导作用,在原文中一些比较少见的词后面我添加了少许译注,希望减少入门兄弟的负担。 
4 由于2中的语言调整和3中的译注,是我的个人行为,可能会有我对 原文理解模糊,概念不清,或是语言组织不顺畅的地方,还请大家一定多多指出来。谢谢。 
个人声明: 
本翻译品受控于chinaunix BSD翻译小组。如需单独转载,请保留“翻译:gvim@chinaunix/bsd”和以上几点说明。



[font=黑体][size=3]4.1 高级进程控制和信号[/size][/font] 
信号:4 a:一种对象,用于传输或负载人类声音之外的信息。 

到目前为止,我们已经讨论了进程的创建和其他系统调用。现在是讨论下面这些问题的时候了:你要在多个进程间通讯以获得更好的进程控制粒度,或者要其他程序或操作者用信号通知你的程序。例如,你可能希望你的程序重新读取它的配置文件。或者,你的数据库程序需要在退出之前将事务从主存写入后备存储器,然后再退出。这两个例子可能只是使用信号的很小一部分。虽然已经有套接字,先入先出队列,管道,信号量等多种方式来完成类似的任务,但是我们将把讨论的焦点放在信号和其它进程控制机制上。在现实中,信号和进程控制机制可以提供大部分你所需要的特性和功能。 

[font=黑体][size=3]4.2 信号[/size][/font] 
信号与硬件中断很相似。当设备需要中断服务的时候它可以产生一个硬件中断来通知CPU。与硬件中断类似的,当进程需要将一些事件通知给其他进程的时候可以使用信号来完成。 

大多数Unix系统管理员会比较熟悉SIGHUP信号。当你通过kill命令向后台服务进程发出SIGHUP信号后,大多数进程要么重新读入他们的配置文件要么重新启动。这些信号之中,一些与硬件有直接关系,如SIGFPE(浮点异常),SIGILL(非法指令);其它则是与软件相关,如SIGSYS(未实现的系统调用被调用)。  

一旦进程接收到信号之后,该信号的行为与信号本身和进程对它的使用目的两个因素有关。一些信号可以被阻塞,忽略,或者捕获,而另外一些则不可以。如果进程需要捕获一个信号并履行一些相关操作,你可以为进程设定这个特定信号的信号处理句柄。处理句柄仅仅是一个函数,在这个信号被进程接收之后调用。或者更确切的说,处理句柄是一个函数调用,你可以对它进行指派(specify)。 

当信号没有指定处理句柄时,将会执行操作系统默认的行为。这些缺省行为可以是从终止进程到完全核心转储等不同的操作。注意,有两个信号不能被捕获或忽略:SIGSTOP和SIGKILL,下面会解释。 

在BSD系统中定义的有许多信号;我们讨论在/usr/include/sys/signals.h(译注:在我的FB5.2.1中是signal.h)文件中定义的标准信号。注意, NetBSD系统中定义的信号数量稍微多一点,并且我们没有将它们的讨论放在这里。所以如果需要使用某个下面没有涉及到的信号的时候,请查阅你的系统的头文件。 

#define SIGHUP          1       /* hangup */

SIGHUP是Unix系统管理员很常用的一个信号。许多后台服务进程在接受到该信号后将会重新读取它们的配置文件。然而,该信号的实际功能是通知进程它的控制终端被断开。缺省行为是终止进程。 
#define SIGINT          2       /* interrupt */

对于Unix使用者来说,SIGINT是另外一个常用的信号。许多shell的CTRL-C组合使得这个信号被大家所熟知。该信号的正式名字是中断信号。缺省行为是终止进程。 
#define SIGQUIT         3       /* quit */

SIGQUIT信号被用于接收shell的CTRL-/组合。另外,它还用于告知进程退出。这是一个常用信号,用来通知应用程序从容的(译注:即在结束前执行一些退出动作)关闭。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGILL          4       /* illegal instr. (not reset when caught) */

如果正在执行的进程中包含非法指令,操作系统将向该进程发送SIGILL信号。如果你的程序使用了线程,或者pointer functions,那么可能的话可以尝试捕获该信号来协助调试。([color=Red]注意:原文这句为:“If your program makes use of use of threads, or pointer functions, try to catch this signal if possible for aid in debugging.”。中间的两个use of use of,不知是原书排版的瑕疵还是我确实没有明白其意义;另外,偶经常听说functions pointer,对于pointer functions,google了一下,应该是fortran里面的东西,不管怎样,还真不知道,确切含义还请知道的兄弟斧正。[/color])缺省行为是终止进程,并且创建一个核心转储。 
#define SIGTRAP         5       /* trace trap (not reset when caught) */

SIGTRAP这个信号是由POSIX标准定义的,用于调试目的。当被调试进程接收到该信号时,就意味着它到达了某一个调试断点。一旦这个信号被交付,被调试的进程就会停止,并且它的父进程将接到通知。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGABRT         6       /* abort() */

SIGABRT提供了一种在异常终止(abort)一个进程的同时创建一个核心转储的方法。然而如果该信号被捕获,并且信号处理句柄没有返回,那么进程不会终止。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGFPE          8       /* floating point exception */

当进程发生一个浮点错误时,SIGFPE信号被发送给该进程。对于那些处理复杂数学运算的程序,一般会建议你捕获该信号。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGKILL         9       /* kill (cannot be caught or ignored) */

SIGKILL是这些信号中最难对付的一个。正如你在它旁边的注释中看到的那样,这个信号不能被捕获或忽略。一旦该信号被交付给一个进程,那么这个进程就会终止。然而,会有一些极少数情况SIGKILL不会终止进程。这些罕见的情形在处理一个“非中断操作”(比如磁盘I/O)的时候发生。虽然这样的情形极少发生,然而一旦发生的话,会造成进程死锁。唯一结束进程的办法就只有重新启动了。缺省行为是终止进程。 
#define SIGBUS          10      /* bus error */

如同它的名字暗示的那样,CPU检测到数据总线上的错误时将产生SIGBUS信号。当程序尝试去访问一个没有正确对齐的内存地址时就会产生该信号。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGSEGV         11      /* segmentation violation */

SIGSEGV是另一个C/C++程序员很熟悉的信号。当程序没有权利访问一个受保护的内存地址时,或者访问无效的虚拟内存地址(脏指针,dirty pointers,译注:由于没有和后备存储器中内容进行同步而造成。关于野指针,可以参见http://en.wikipedia.org/wiki/Wild_pointer 的解释。)时,会产生这个信号。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGSYS          12      /* non-existent system call invoked */

SIGSYS信号会在进程执行一个不存在的系统调用时被交付。操作系统会交付该信号,并且进程会被终止。缺省行为是终止进程,并且创建一个核心转储。 
#define SIGPIPE         13      /* write on a pipe with no one to read it */

管道的作用就像电话一样,允许进程之间的通信。如果进程尝试对管道执行写操作,然而管道的另一边却没有回应者时,操作系统会将SIGPIPE信号交付给这个讨厌的进程(这里就是那个打算写入的进程)。缺省行为是终止进程。 
#define SIGALRM         14      /* alarm clock */

在进程的计时器到期的时候,SIGALRM信号会被交付(delivered)给进程。这些计时器由本章后面将会提及的setitimer和alarm调用设置。缺省行为是终止进程。 
#define SIGTERM         15      /* software termination signal from kill */

SIGTERM信号被发送给进程,通知该进程是时候终止了,并且在终止之前做一些清理活动。SIGTERM信号是Unix的kill命令发送的缺省信号,同时也是操作系统关闭时向进程发送的缺省信号。缺省行为是终止进程。 
#define SIGURG          16      /* urgent condition on IO channel */

在进程已打开的套接字上发生某些情况时,SIGURG将被发送给该进程。如果进程不捕获这个信号的话,那么将被丢弃。缺省行为是丢弃这个信号。 
#define SIGSTOP         17      /* sendable stop signal not from tty */

本信号不能被捕获或忽略。一旦进程接收到SIGSTOP信号,它会立即停止(stop),直到接收到另一个SIGCONT信号为止。缺省行为是停止进程,直到接收到一个SIGCONT信号为止。 
#define SIGTSTP         18      /* stop signal from tty */

SIGSTP与SIGSTOP类似,它们的区别在于SIGSTP信号可以被捕获或忽略。当shell从键盘接收到CTRL-Z的时候就会交付(deliver)这个信号给进程。缺省行为是停止进程,直到接收到一个SIGCONT信号为止。 
#define SIGCONT         19      /* continue a stopped process */

SIGCONT也是一个有意思的信号。如前所述,当进程停止的时候,这个信号用来告诉进程恢复运行。该信号的有趣的地方在于:它不能被忽略或阻塞,但可以被捕获。这样做很有意义:因为进程大概不愿意忽略或阻塞SIGCONT信号,否则,如果进程接收到SIGSTOP或SIGSTP的时候该怎么办?缺省行为是丢弃该信号。 
#define SIGCHLD         20      /* to parent on child stop or exit */

SIGCHLD是由Berkeley Unix引入的,并且比SRV 4 Unix上的实现有更好的接口。(如果信号是一个没有追溯能力的过程(not a retroactive process),那么BSD的SIGCHID信号实现会比较好。在system V Unix的实现中,如果进程要求捕获该信号,操作系统会检查是否存在有任何未完成的子进程(这些子进程是已经退出(exit)的子进程,并且在等待调用wait的父进程收集它们的状态)。如果子进程退出的时候附带有一些终止信息(terminating information),那么信号处理句柄就会被调用。所以,仅仅要求捕获这个信号会导致信号处理句柄被调用(译注:即是上面说的“信号的追溯能力”),而这是却一种相当混乱的状况。) 

一旦一个进程的子进程状态发生改变,SIGCHLD信号就会被发送给该进程。就像我在前面章节提到的,父进程虽然可以fork出子进程,但没有必要等待子进程退出。一般来说这是不太好的,因为这样的话,一旦进程退出就可能会变成一个僵尸进程。可是如果父进程捕获SIGCHLD信号的话,它就可以使用wait系列调用中的某一个去收集子进程状态,或者判断发生了什么事情。当发送SIGSTOP,SIGSTP或SIGCONF信号给子进程时,SIGCHLD信号也会被发送给父进程。缺省行为是丢弃该信号。 
#define SIGTTIN         21      /* to readers pgrp upon background tty read */

当一个后台进程尝试进行一个读操作时,SIGTTIN信号被发送给该进程。进程将会阻塞直到接收到SIGCONT信号为止。缺省行为是停止进程,直到接收到SIGCONT信号。 
#define SIGTTOU         22      /* like TTIN if (tp->t_local&LTOSTOP) */

SIGTTOU信号与SIGTTIN很相似,不同之处在于SIGTTOU信号是由于后台进程尝试对一个设置了TOSTOP属性的tty执行写操作时才会产生。然而,如果tty没有设置这个属性,SIGTTOU就不会被发送。缺省行为是停止进程,直到接收到SIGCONT信号。 
#define SIGIO           23      /* input/output possible signal */

如果进程在一个文件描述符上有I/O操作的话,SIGIO信号将被发送给这个进程。进程可以通过fcntl调用来设置。缺省行为是丢弃该信号。 
#define SIGXCPU         24      /* exceeded CPU time limit */

如果一旦进程超出了它可以使用的CPU限制(CPU limit),SIGXCPU信号就被发送给它。这个限制可以使用随后讨论的setrlimit设置。缺省行为是终止进程。 
#define SIGXFSZ         25      /* exceeded file size limit */
如果一旦进程超出了它可以使用的文件大小限制,SIGXFSZ信号就被发送给它。稍后我们会继续讨论这个信号。缺省行为是终止进程。 
#define SIGVTALRM       26      /* virtual time alarm */

如果一旦进程超过了它设定的虚拟计时器计数时,SIGVTALRM信号就被发送给它。缺省行为是终止进程。 
#define SIGPROF         27      /* profiling time alarm */

当设置了计时器时,SIGPROF是另一个将会发送给进程的信号。缺省行为是终止进程。 
#define SIGWINCH        28      /* window size changes */

当进程调整了终端的行或列时(比如增大你的xterm的尺寸),SIGWINCH信号被发送给该进程。缺省行为是丢弃该信号。 
#define SIGUSR1         29      /* user defined signal 1 */

#define SIGUSR2         30      /* user defined signal 2 */

SIGUSR1和SIGUSR2这两个信号被设计为用户指定。它们可以被设定来完成你的任何需要。换句话说,操作系统没有任何行为与这两个信号关联。缺省行为是终止进程。(译注:按原文的意思翻译出来似乎这两句话有点矛盾。) 

[font=黑体][size=-1]4.3 系统调用[/size][/font] 
那么,你该如何使用信号呢?有时候甚至拿不准是否应该使用信号。例如,当信号被交付的时候,一方面你可以在行为发生之前,分析当前情况,找出信号发生的原因,或者找到这些信号是从哪里发出来的;另一方面,其他一些时候你也可以只是希望简单的退出程序,并且在清除之后创建一个核心转储文件。参见最后部分的简单代码可以获得这些函数的较详细的例子。 

[font=黑体]Kill函数[/font] 
kill函数对于那些经常在命令行使用kill命令杀死进程的人来说是再熟悉不过的。基本语法是: 
   
 int   kill(pid_t pid, int sig);

Kill函数将指定的信号发送给进程号为pid的进程。只有当进程符合下面几点情况的时候信号才会被交付: 
引用:&#8226; 发送与接收进程有相同的有效用户ID(UID); 
&#8226; 发送进程有适当的权限(例如:setuid命令); 
&#8226; 发送进程有超级用户(root)的UID。


注意:SIGCONT信号是一个特例,它可以由当前进程发送给任何一个该进程的派生进程。 

使用不同的调用参数使得kill函数的行为差别非常大。这些行为如下所述: 
(译注:下面的PID应该指的是上面kill函数原型中的那个pid,我在这里做出说明并保留原文) 
引用:&#8226; 如果PID大于0,并且发送进程有适当的权限,那么参数sig指定的信号将被交付。 
&#8226; 如果PID等于0,那么sig信号将被交付给所有那些与发送进程有相同组ID的进程。(发送进程同样需要满足权限需求。) 
&#8226; 如果PID是 -1,那么信号将被发送给所有那些与发送进程有相同有效用户ID的进程(不包含发送进程在内)。然而,如果发送进程的有效用户ID与超级用户(root)的相同,那么信号被交付给除了系统进程(由它们的proc结构中的p_flag域是否是P_SYSTEM来定义)之外的所有进程。在这个特殊的例子中,如果某些进程不能被发送(could not be sent)sig信号,kill函数并不返回一个错误。 
&#8226; 如果sig是0,kill函数只检查错误(例如,无效权限,不存在的进程等)。该用法有时候用来检查一个指定进程是否存在。 
&#8226; 如果成功的话kill函数返回0,否则返回-1。kill调用失败时会在errno全局变量中设置相应的错误值。



kill的另一个版本是raise函数: 
    
int    raise(int sig);

raise函数会向当前进程发送sig信号。该函数用处不是很大,因为它只能够给当前进程发送信号。raise函数调用成功时返回0,否则返回-1。调用失败时会在errno全局变量中设置相应的错误值,效果和signal函数的返回类似:(译注:原文只有两个单词“as in:”,我并不知道作者把signal列在这里所要表达的意思,所以我按照我的理解+猜测来翻译的。如果大家有什么建议,或是需要纠正的话,请一定告诉我。) 
   
void (*signal(int sig, void (*func)(int)))(int);


4.4 信号处理 
现在我们知道何时会产生信号,也知道如何发送信号,那么我们怎么处理它们呢? 

[font=黑体]signal函数[/font] 

signal系统函数调用提供了一种最简单的范例。然而,由于C原形声明的缘故使它看起来比实际复杂。signal函数将一个给定的函数和一个特定的信号联系。这里是FreeBSD中的定义(和一个typedef一起): 
引用: 
typedef void (*sig_t) (int); 

sig_t   signal(int sig, sig_t func); 



第一个参数是目标信号,可以是上面列举的所有信号中的任何一个。func参数是一个指针,指向某个处理该信号的函数。这个处理信号函数带有一个int型参数,并应返回void。signal函数中的func参数也可以设定为下面的一些值: 
引用: 
SIG_IGN: 如果func参数被设置为SIG_IGN,该信号将被忽略。 

SIG_DFL: 如果func参数被设置为SIG_DFL,该信号会按照确定行为处理。 




[font=黑体]sigaction函数[/font] 

sigaction函数是一个比signal更通用的方案。第一个参数是目标信号。下一个名为act的参数(指向)sigaction结构,该结构包含一些用于信号处理的信息。最后一个参数oact是一个指针,指向一个可以存储上一次设置信号处理的信息的地方。 
引用: 
int  sigaction(int sig, const struct sigaction *act, struct sigaction *oact); 



sigaction结构有下面这些个成员: 
引用:void     (*sa_handler)(int);

这个结构成员是一个指向函数的指针,该函数带有一个简单的整形参数,并返回(void)。这与signal函数的func参数相同,也可以被设置为SIG_IGN和SIG_DFL,并且与调用signal得到的效果也一样。 
引用:void     (*sa_sigaction)(int, siginfo_t *, void *);

该结构成员是一个指向函数的指针,返回(void)并需要三个参数。这些参数依次为:一个整形参数指定信号发送;一个指向siginfo_t结构的指针用来保存关于信号的信息;最后一个也是一个指针,指向信号交付时的特定上下文(context)空间。 
引用:sigset_t sa_mask;

该结构成员是一个位掩码(bitwise mask),用来指示信号交付时哪些信号会被阻塞。阻塞SIGKILL和SIGSTOP信号的做法会被忽略。接下来,被阻塞的信号将被推迟,直到它们被开启(unblock)。参见sigprocmask获得更多关于全局掩码(global masks)的信息。 
引用:int      sa_flags;

该数据成员是一个拥有下面这些标志的位掩码: 
引用: 
SA_NOCLDSTOP: 如果SA_NOCLDSTOP位被置位并且目标信号是SIGCHLD,除非子进程退出,而在子进程停止(stop)时父进程将不会收到通知。 

SA_NOCLDWAIT: SA_NOCLDWAIT标志会阻止子进程成为僵尸进程。在目标信号是SIGCHLD的时候使用。如果进程设置了这个标志,接着调用某个wait系统调用,进程将被阻塞直到子进程全部终止,最后返回-1(译注:此处在APUE2ed中的解释是返回1),设置errno全局变量为ECHILD。 

SA_ONSTACK: 一些时候需要在特定的堆栈上进行信号的处理。sigaction系统调用提供了这个方式。如果该位被置位,那么信号将会被交付到指定的堆栈上。 

SA_NODEFER: 如果SA_NODEFER位被置位,那么当前信号正被处理时,系统不会屏蔽该信号以后的交付。 

SA_RESETHAND: 如果SA_RESETHAND被置位,一旦信号被交付,信号处理句柄将被置为SIG_DEF。 

SA_SIGINFO: 被置位时,由结构体sigaction 的成员sa_sigaction指向的函数被使用。注意:使用SIG_IGN或SIG_DFL时不应该设置这个标志。成功调用sigaction之后,返回0或-1,并且将error设置成相关错误值。



4.5信号掩码(阻塞与开启信号) 

进程可以阻塞或设置某个信号。一旦该信号被阻塞,关于它的交付将被推迟,直到进程重新开启它。在这样的情况下是非常有用的:进程进入代码中某个部分,不能被中断但仍希望可以接受、处理可能丢失的信号。可靠交付信号的能力直到4.2BSD引入之后(不久被SVR3采用),操作系统才拥有该能力。 

随着可靠信号的出现,信号的生命和交付(life and delivery)都有所改变。信号可以在之前产生和交付。现在,一旦信号是挂起的(pending),进程可以在接收它之前决定怎么处理。进程可能会去处理它,也可能设置为缺省行为,或者丢弃信号 

注意:如果许多信号都挂起,系统将会首先交付会改变进程状态的信号,例如SIGBUS。 

[font=黑体]sigprocmask[/font] 

任何进程可以使用sigprocmask函数来阻塞信号。语法如下: 
引用:int     sigprocmask(int how, const sigset_t *set, sigset_t *oset);

sigprocmask函数会修改或检查(modify or examine)当前信号掩码。当set参数不是null的时候,sigprocmask的行为和第一个参数how有关。函数行为和相关意义列举如下: 
引用: 
SIG_BLOCK: 在set参数中指定的信号被阻塞,并且添加进阻塞信号列表。 

SIG_UNBLOCK: 在set参数中指定的信号会从信号掩码中移除。 

SIG_SETMASK: set参数将完全替代当前信号掩码。如果oset参数不为null,则会被设置为前一个信号掩码。如果set值是null,how参数被忽略并且信号掩码保持不变。所以,为了检查信号掩码,我们可以将传入set null值,oset为非null值来调用sigprocmask函数。一旦掩码得到之后,你可能需要对他进行检查或操作。可以使用下面的过程(routine)。注意当前这些过程是宏的实现。 



int    sigemptyset(sigset_t *set)

如果调用这个过程,set参数将被初始化指向一个空信号集。 
int    sigfillset(sigset_t *set)

如果调用这个过程,set参数将被初始化指向一个包括所有信号的信号集。 
int   sigaddset(sigset_t *set, int signo)

如果调用这个过程,signo指定的信号将被添加进set参数指定的信号集。 
int    sigdelset(sigset_t *set, int signo)

如果调用这个过程,signo指定的信号将从set参数指定的信号集中移除。 
int   sigismember(const sigset_t *set, int signo)

如果调用这个过程,如果由signo指定的信号存在于set参数指定的信号集中时,返回1,否则返回0。 
int     sigpending(sigset_t *set);

进程可以使用sigpending函数去查出当前那些信号被挂起。sigpending函数会返回一个包含所有挂起信号的掩码。该掩码可以使用上面介绍的过程去检查。sigpending成功时返回0,否则返回-1,并且设置errno为相应错误值。 

[size=3][font=黑体]4.6 自定义行为[/font][/size] 
一些时候程序要求信号处理句柄运行在一个特定的堆栈上。为了实现这个目的,一个备用(alternate)堆栈区间必须用signaltstack函数指出来。这个函数使用的数据结构为signaltstack: 
int    sigaltstack(const struct sigaltstack *ss, struct sigaltstack *oss);

它的结构成员解释如下。 
char    *ss_sp;
  
该成员指向一个被用作堆栈的区域。系统中有个MINSIGSTKSZ常量,它定义了进行信号处理时所需的最小内存空间。系统中还有一个SIGSTKSZ常量,它定义了通常情况下处理时所需内存空间。该内存空间需要在调用signaltstack函数之间分配。 
size_t  ss_size;

数据成员ss_size指出新堆栈的大小。如果这个值是错误的(inaccurate),当信号处理句柄执行时,它的行为就变得不可预知(你不能明确知道系统怎样处理这个信号)。 
int     ss_flags;
  
根据调用环境(calling circumstances),数据成员ss_flags可以具有少数几个不同的值。首先,当进程希望停用备用堆栈的时候,ss_flags会被设为SS_DISABLE。在这个情况下,ss_sp和ss_size被忽略,备用堆栈被禁止。注意,备用堆栈只能在当前句柄没有处理时禁止。 
  
如果使用一个non-null值作为oss的实参去调用signaltstack,ss_flags将包含指示当前状态的信息。它们是: 
 SS_DISABLE: 备用堆栈被停用。

SS_ONSTACK: 备用堆栈当前正在被使用,并且现在不可以停用。
  
如果调用signaltstack 的oss实参不是null,会返回当前状态。调用成功返回0,否则返回-1。如果调用失败,errno也会相应的被设置。因为信号可以在任何一点被交付,所以很难被预测。出于这个原因,4.2BSD的缺省行为是:重新开始被中断的系统调用,重新提供还没有被转送的数据。在大多数时候这个行为是很不错的,并且也是所有BSD系统采用的缺省行为。可是,也有一些罕见的情况,你可能需要将这个特性关掉。你可以使用siginterrupt函数完成需求。使用很简单: 
int   siginterrupt(int sig, int flag);

将sig参数设置为目标信号,并且设置flag为真(在这个情况下是1)。如果flag参数被设为假(在这个情况下是0),那么缺省行为是重新启动系统调用。 

[align=center][size=3][font=黑体]4.7 等待信号[/font][/size][/align] 
sigsuspend函数可以暂时将当前阻塞信号集改变为由sigmask指定的信号集。改变后,sigsuspend会等待,直到一个信号被交付。一旦一个信号被交付后,原先的信号集被恢复。由于sigsuspend调用在信号交付之后总是被终止,它的返回值总是-1,errno总是EINTR。下面是它的语法: 
int     sigsuspend(const sigset_t *sigmask);
  
sigwait函数用set参数指定的信号集作为信号掩码。它会检查包含在这个特定集合内的是否有任何挂起信号,如果有的话,它将清除这个挂起的信号,并在sig参数中返回这个被清除信号的数值。如果没有信号挂起,sigwait将一直等待直到指定信号集合中的任何一个信号产生。下面是它的语法: 
int    sigwait(const sigset_t *set, int *sig);
  
当信号被交付给进程(该进程安装了相应信号的处理句柄)时,进程将会切换到信号处理句柄中执行。例如,假设你的程序监听一个由配置文件设定的端口。你的进程安装了一个捕获SIGHUP信号的处理句柄来重新读取配置文件。一旦SIGHUP信号被交付给你的程序,进程将会执行信号处理句柄来重新读取配置文件。这里存在一个问题,你没有办法知道在进程执行过程中信号被交付的确切地点。你虽然可以使用一些下面列出的函数来将范围缩小,但是如果碰到像打开套接字,打开链接,或者其它那些首先需要清理后才能在新端口上监听 这些情况的时候呢?你怎样确定清理活动在那里开始,什么时候开始?如果你的程序正在等到输入,并且没有数据被传进来的时候,系统调用将被重启(system call will be restarted),所以从SIGHUP的返回将会继续等待。 
  
这是使用setjmp和longjmp函数的一些情况,这些函数提供非本地分支(non-local branching)。为了使用这些setjmp函数,需要提供一个evn参数,如下: 
   
jmp_buff  env;

  int    sigsetjmp(sigjmp_buf env, int savemask);

  void   siglongjmp(sigjmp_buf env, int val);

  int    setjmp(jmp_buf env);

  void   longjmp(jmp_buf env, int val);

  int     _setjmp(jmp_buf env);

  void    _longjmp(jmp_buf env, int val);

  void    longjmperror(void);

  
首先,调用setjmp的返回为0,当前的环境将被保存在env中。接着,你可以在信号处理句柄内部调用对应的longjmp。一旦调用了longjmp,它将把执行环境恢复为env中保存的环境,并返回到最初setjmp被调用时的环境中。最初的setjmp调用返回那个传递给longjmp的va参数的值。 
  
关于setjmp和longjmp函数一些说明:首先,这两个是不能混杂使用。也就是说,调用setjmp时保存的env变量不能传递给_longjmp调用。另外,调用setjmp的函数返回后,接下来调用longjmp将会发生错误。 
The different calls have specific actions that they take. These actions are listed below:  
不同的调用有特定的行为。这些行为在下面列出来: 
jmp和longjmp: 他们会保存(恢复)信号掩码,寄存器组和堆栈。

_setjmp和_longjmp: 他们只保存(恢复)寄存器组和堆栈。

sigsetjmp和siglongjmp: 只要savemask参数不是0,他们就保存(恢复)寄存器组,堆栈和信号掩码。

由于一些原因,如果env参数保存的东西被破坏,或者调用setjmp的函数返回了,longjmp函数将会调用longjmperror函数。如果longjmperror也返回了,程序将被异常终止。你可以使用与longjmperror有相同原形的函数来自定义longjmperror函数。缺省的longjmperror会在标准错误上输出”longjmp botch”,然后返回。 
4.8 Alarms  
 unsigned int   alarm(unsigned int seconds);
  
alarm函数基本上是一个简单的闹钟时钟(alarm clock),同时也是一个很有用的函数。它允许进程在经过指定秒数之后收到一个通知。一旦闹钟时间到,进程将收到一个SIGALRM信号。任何随后的alarm调用都会覆盖原先的调用设定。alarm不像sleep函数,它不会被阻塞。 

它有一些返回值需要值得你注意:首先,如果进程没有设定定时器,那么返回值是0。其次,如果有一个定时器被设定但还没有超时的话,那么会返回前一个调用到现在还有的剩余时间。 

现在可以设定的最大时间是100,000,000秒 --- 已经是相当长的时间了。 
int getitimer(int which, struct itimerval *value);


getitimer函数会检索由第一个参数(which参数)描述的itimerval结构。第一个参数可选的选项将在下面说明: 
int  setitimer(int which, const struct itimerval *value,  struct itimerval *ovalue);
  
setitmer比先前的alarm调用提供了更稳定的接口。在BSD系统上,每个进程可以提供三种不同时间间隔的定时器。他们在下面讲述: 
#define ITIMER_REAL      0

实时时钟实时的递减而不管进程在CPU上的实际花销时间(换句话说,它追踪自然时间natural time)。这允许进程设置一个基于自然实时时间(based on  atural real time)的定时器。当实时定时器超时的时候,进程会收到SIGALRM信号。 
#define ITIMER_VIRTUAL   1
  
虚拟定时器仅只递减进程在CPU上的执行时间,允许进程设定一个基于CPU使用率的定时器。当虚拟定时器超时的时候,进程收到SIGVTALRM信号 
#define ITIMER_PROF      2

Profile定时器递减在CPU上的执行时间和代表进程执行的系统调用的时间。这对于那些要求静态剖析的解释程序是很有帮助的。当profile定时器超时的时候,进程收到SIGPROF信号。然而并不像实时和虚拟定时器那样,SIGPROF可以在系统调用的时候被发送;进程应该准备好重新执行被中断的系统调用。 

本章将焦点放在了信号库上。这些信号及他们的使用方法对于系统编程是很重要的。信号允许系统管理员通知应用程序重新读取配置文件,从而使程序更稳定。其它重要的信号处理挂起在打开的文件描述符上的I/O操作。下一章讲述怎样利用这些I/O相关的信号。 



[align=center]第五章 基本输入输出[/align] 
[align=center]译者:horseman 
(多谢我的同事HUGH CHEN翻译5.6,5.7跟部分5.4,5.5段落)[/align] 

本章示范(示例)代码:http://www.khmere.com/freebsd_book/src/05/index.html 

Tags:freebsd 系统 编程
关于开源中文网 - 联系我们 - 广告服务 - 网站地图 - 版权声明