1、进程进程1.父子进程如何通信?一对父子进程可以通过正常的进程间通信的办法(管道,套接字,消息队列,共享内存)进行通信,但也可以通过利用它们作为父子进程的相互关系而具有的一些特殊方法。一个最显然的方法是父进程可以得到子进程的退出状态。因为子进程从它的父进程继承文件描述符,所以父进程可以打开一个管道的两端,然后fork,然后父进程关闭管道这一端,子进程关闭管道另一端。这正是你从你的进程调用popen()函数运行另一个程序所发生的情况,也就是说你可以向popen()返回的文件描述符进行写操作而子进程将其当作自己的标准输入,或者你可以读取这个文件描述符来看子进程向标准输出写了什么。(popen()函数
2、的mode参数定义你的意图(译者注:mode=“r”为读,mode=“w”为写);如果你想读写都做,那么你可以并不困难地用管道自己做到。而且,子进程继承由父进程用mmap函数映射的匿名共享内存段(或者通过映射特殊文件/dev/zero);这些共享内存段不能从无关的进程访问。2. 我怎样去除僵死进程?2.1何为僵死进程?当一个程序创建的子进程比父进程提前结束,内核仍然保存一些它的信息以便父进程会需要它 - 比如,父进程可能需要检查子进程的退出状态。为了得到这些信息,父进程调用wait();当这个调用发生,内核可以丢弃这些信息。在子进程终止后到父进程调用wait()前的时间里,子进程被称为僵死进程
3、(zombie)。(如果你用ps,这个子进程会有一个Z出现在它的状态区里指出这点。)即使它没有在执行,它仍然占据进程表里一个位置。(它不消耗其它资源,但是有些工具程序会显示错误的数字,比如中央处理器的使用;这是因为为节约空间进程表的某些部份与会计数据(accounting info)是共用(overlaid)的。)这并不好,因为进程表对于进程数有固定的上限,系统会用光它们。即使系统没有用光 ,每一个用户可以同时执行的进程数有限制,它总是小于系统的限制。顺便说一下,这也正是你需要总是 检查fork()是否失败的一个原因。如果父进程未调用wait函数而终止,子进程将被init进程收管,它将控制子进
4、程退出后必须的清除工作。(init是一个特殊的系统程序,进程号为1 - 它实际上是系统启动后运行的第一个程序),2.2我怎样避免它们的出现?你需要却认父进程为每个子进程的终止调用wait()(或者waitpid(),wait3(),等等); 或者,在某些系统上,你可以指令系统你对子进程的退出状态没有兴趣。(译者注:在SysV系统上,可以调用signal函数,设置SIGCLD信号为SIG_IGN,系统将不产生僵死进程, 详细说明参见10.7节)另一种方法是*两次*fork(),而且使紧跟的子进程直接退出,这样造成孙子进程变成孤儿进程(orphaned),从而init进程将负责清除它。欲获得做这个
5、的程序,参看范例章节的函数fork2()。为了忽略子进程状态,你需要做下面的步骤(查询你的系统手册页以知道这是否正常工作):struct sigaction sa;sa.sa_handler = SIG_IGN;#ifdef SA_NOCLDWAITsa.sa_flags = SA_NOCLDWAIT;#elsesa.sa_flags = 0;#endifsigemptyset(&sa.sa_mask);sigaction(SIGCHLD, &sa, NULL);如果这是成功的,那么wait()函数集将不再正常工作;如果它们中任何一个被调用,它们将等待直到*所有*子进程已经退出,然后返回失败,
6、并且errno=ECHILD。另一个技巧是捕获SIGCHLD信号,然后使信号处理程序调用waitpid()或wait3()。参见范例章节的完整程序。3 我怎样使我的程序作为守护程序运行?一个“守护程序”进程通常被定义为一个后台进程,而且它不属于任何一个终端会话,(terminal session)。许多系统服务由守护程序实施;如网络服务,打印等。简单地在后台启动一个程序并非足够是这些长时间运行的程序;那种方法没有正确地将进程从启动它的终端脱离(detach)。而且,启动守护程序的普遍接受的的方法是简单地手工执行或从rc脚本程序执行(译者注:rc:runcom);并希望这个守护程序将其*自身*安
7、置到后台。这里是成为守护程序的步骤:1. 调用fork()以便父进程可以退出,这样就将控制权归还给运行你程序的命令行或shell程序。需要这一步以便保证新进程不是一个进程组头领进程(process group leader)。下一步,setsid(),会因为你是进程组头领进程而失败。2. 调用setsid() 以便成为一个进程组和会话组的头领进程。由于一个控制终端与一个会话相关联,而且这个新会话还没有获得一个控制终端,我们的进程没有控制终端,这对于守护程序来说是一件好事。3. 再次调用fork()所以父进程(会话组头领进程)可以退出。这意味着我们,一个非会话组头领进程永远不能重新获得控制终端。
8、4. 调用chdir(/)确认我们的进程不保持任何目录于使用状态。不做这个会导致系统管理员不能卸装(umount)一个文件系统,因为它是我们的当前工作目录。类似的,我们可以改变当前目录至对于守护程序运行重要的文件所在目录5. 调用umask(0)以便我们拥有对于我们写的任何东西的完全控制。我们不知道我们继承了什么样的umask。这一步是可选的(译者注:这里指步骤5,因为守护程序不一定需要写文件)6. 调用close()关闭文件描述符0,1和2。这样我们释放了从父进程继承的标准输入,标准输出,和标准错误输出。我们没办法知道这些文描述符符可能已经被重定向去哪里。注意到许多守护程序使用sysconf
9、()来确认_SC_OPEN_MAX的限制。_SC_OPEN_MAX告诉你每个进程能够打开的最多文件数。然后使用一个循环,守护程序可以关闭所有可能的文件描述符。你必须决定你需要做这个或不做。如果你认为有可能有打开的文件描述符,你需要关闭它们,因为系统有一个同时打开文件数的限制。7. 为标准输入,标准输出和标准错误输出建立新的文件描述符。即使你不打算使用它们,打开着它们不失为一个好主意。准确操作这些描述符是基于各自爱好;比如说,如果你有一个日志文件,你可能希望把它作为标准输出和标准错误输出打开,而把/dev/null作为标准输入打开;作为替代方法,你可以将/dev/console作为标准错误输出和
10、/或标准输出打开,而/dev/null作为标准输入,或者任何其它对你的守护程序有意义的结合方法。(译者注:一般使用dup2函数原子化关闭和复制文件描述符,参见3.12节)如果你的守护程序是被inetd启动的,几乎所有这些步骤都不需要(或不建议采用)。在那种情况下,标准输入,标准输出和标准错误输出都为你指定为网络连接,而且fork()的调用和会话的操纵不应做(以免使inetd造成混乱)。只有chdir()和umask()这两步保持有用。4我怎样象ps程序一样审视系统的进程?你真的不该想做这个。到目前为止,移植性最好的是调用popen(pscmd,r)并处理它的输出。(pscmd应当是类似SysV
11、系统上的“ps -ef”,BSD系统有很多可能的显示选项:选择一个。)在范例章节有这个问题的两个完整解决方法;一个适用于SunOS 4,它需要root权限执行并使用kvm_*例程从内核数据结果读取信息;另一种适用于SVR4系统(包括Sun OS 5),它使用/proc文件系统。在具有SVR4.2风格/proc的系统上更简单;只要对于每一个感兴趣的进程号从文件/proc/进程号/psinfo读取一个psinfo_t结构。但是,这种可能是最清晰的方法也许又是最不得到很好支持的方法。(在FreeBSD的/proc上,你从/proc/进程号/status读取一个半未提供文档说明(semi-undocu
12、mented)的可打印字符串;Linux有一些与其类似的东西)5 给定一个进程号,我怎样知道它是个正在运行的程序?使用kill()函数,而已0作为信号代码(signal number)。从这个函数返回有四种可能的结果:* kill()返回0- 这意味着一个给定此进程号的进程退出,系统允许你向它发送信号。该进程是否可以是僵死进程与不同系统有关。* kill()返回-1,errno = ESRCH- 要么不存在给定进程号的进程,要么增强的安全机制导致系统否认它的存在。(在一些系统上,这个进程有可能是僵死进程。)* kill()返回-1,errno = EPERM- 系统不允许你杀死(kill)这个
13、特定进程。这意味着要么进程存在(它又可能是僵死进程),要么严格的增强安全机制起作用(比如你的进程不允许发送信号给*任何人*)。* kill()返回-1,伴以其它errno值- 你有麻烦了!用的最多的技巧是认为调用“成功”或伴以EPERM的“失败”意味着进程存在,而其它错误意味着它不存在。如果你特别为提供/proc文件系统的系统(或所有类似系统)写程序,一个替换方法存在:检查proc/进程号是否存在是可行的。6 system函数,pclose函数,waitpid函数 的返回值是什么? system(),pclose()或者waitpid()的返回值不象是我进程的退出值(exit value)(译
14、者注:退出值指调用exit() 或_exit()时给的参数). 或者退出值左移了8位.这是怎么搞的?手册页是对的,你也是对的! 如果查阅手册页的waitpid()你会发现进程的返回值被编码了。正常情况下,进程的返回值在高16位,而余下的位用来作其它事。如果你希望可移植,你就不能凭借这个,而建议是你该使用提供的宏。这些宏总是在wait()或wstat的文档中说明了。为了不同目的定义的宏(在)包括(stat是waitpid()返回的值):WIFEXITED(stat)如果子进程正常退出则返回非0WEXITSTATUS(stat)子进程返回的退出码WIFSIGNALED(stat)如果子进程由与信号
15、而 终止则返回非0WTERMSIG(stat)终止子进程的信号代码WIFSTOPPED(stat)如果子进程暂停(stopped)则返回非0WSTOPSIG(stat)使子进程暂停的信号代码WIFCONTINUED(stat)如果状态是表示子进程继续执行则返回非0WCOREDUMP(stat)如果WIFSIGNALED(stat)为非0,而如果这个进程产生一个内存映射文件(core dump)则返回非07 我怎样找出一个进程的存储器使用情况?如果提供的话,参看getrusage()手册页7.1 为什么进程的大小不缩减?当你使用free()函数释放内存给堆时,几乎所有的系统都*不*减少你程序的对
16、内存的使用。被free()释放的内存仍然属于进程地址空间的一部份,并将被将来的malloc()请求所重复使用。如果你真的需要释放内存给系统,参看使用mmap()分配私有匿名内存映射(private anonymous mappings)。当这些内存映射被取消映射时,内存真的将其释放给系统。某些malloc()的实现方法(比如在GNU C库中)在允许时自动使用mmap()实施大容量分配;这些内存块(blocks)随着free()被释放回系统。当然,如果你的程序的大小增加而你认为它不应该这样,你可能有一个内存泄露(memory leak)- 即在你的的程序中有缺陷(bug)导致未用的内存没释放。7
17、.2 我怎样改变我程序的名字(即“ps”看到的名字)?在BSD风格的系统中,ps程序实际上审视运行进程的地址空间从而找到当前的argv,并显示它。这使得程序可以通过简单的修改argv以改变它的名字。在SysV风格的系统中,命令的名字和参数的一般头80字节是存放在进程的u-区(u-area), 所以不能被直接修改。可能有一个系统调用用来修改它(不象是这样),但是其它的话,只有一个方法就是实施一个exec(),或者些内核内存(危险,而且只有root才有可能)。一些系统(值得注意的是Solaris)可以有ps的两种不同版本,一种是在/usr/bin/ps拥有SysV的行为,而另一种在/usr/ucb
18、/ps拥有BSD的行为。在这些系统中,如果你改变argv,那么BSD版的ps将反映这个变化,而SysV版将不会。检查你的系统是否有一个函数setproctitle()。7.3 我怎样找到进程的相应可执行文件?这个问题可以作为常见未回答问题(Frequently Unanswered Questions)的一个好候选,因为事实上提出这个问题经常意味着程序的设计有缺陷。:)你能作的最佳猜测(best guess)是通过审视argv0的值而获得。如果它包括一个/,那么它可能是可执行程序的绝对或相对(对于在程序开始时的当前目录而言)路径。如果不包括,那么你可以仿效shell对于PATH变量的查询来查找
19、这个程序。但是,不能保证成功,因为有可能执行程序时argv0是一些任意值,也不排除这个可执行文件在执行后可能已经被更名或删除的情况。如果所有你想做的只是能打印一个和错误消息一起出现的合适的名字,那么最好的方法在main()函数中将argv0的值保存在全局变量中以供整个程序使用。虽然没有保证说argv0的值总是有意义,但在大多数情况下它是最好的选择。人们询问这个问题的最普通原因是意图定位他们程序的配置文件。这被认为是不好的形式;包含可执行文件的目录应当*只*包含可执行文件,而且基于管理的要求经常试图将配置文件放置在和可执行文件不同的文件系统。试图做这个的一个比较不普通但更正规的理由是允许程序调用
20、exec()执行它自己;这是一种用来完全重新初始化进程(比如被用于一些sendmail的版本)的办法(比如当一个守护程序捕获一个SIGHUP信号)。7.4 那么,我把配置文件放在哪里里呢?为配置文件安排正确的目录总是取决于你使用的Unix系统的特点;/var/opt/PACKAGE,/usr/local/lib,/usr/local/etc,或者任何其它一些可能的地方。用户自定义的配置文件通常是在$HOME下的以“.”开始的隐藏文件(比如$HOME/.exrc)。从一个在不同系统上都能使用的软件包(package)的角度看,它通常意味着任何站点范围(sitewide)的配置文件的位置有个已设定
21、的缺省值,可能情况是使用一个在配置脚本程序里的-prefix选项(Autoconf 脚本程序集做这个工作)。你会希望允许这个缺省值在程序执行时被一个环境变量重载。(如果你没使用配置脚本程序,那么在编译时,将这个位置缺省值作为-D选项放入项目文件(Makefile),或者将其放入一个config.h头文件,或做其它类似的工作)用户自定义配置需要放置于一个在$HOME下的文件名“.”打头的文件,或者在需要多个配置文件时,建立文件名“.”打头的子目录。(在列目录时,文件名以 “.”打头的文件或目录缺省情况下被忽略。)避免在$HOME建立多个文件,因为这会造成非常杂乱的情况。当然,你也应该允许用户通过
22、一个环境变量重载这个位置。即使不能找到某个用户的配置文件,程序仍应当以适宜的方式执行。7.5 为何父进程死时,我的进程未得到SIGHUP信号?因为本来就没有设想是这样做的。 SIGHUP是一个信号,它按照惯例意味着“终端线路被挂断”。它与父进程无关,而且通常由tty驱动程序产生(并传递给前台的进程组)。但是,作为会话管理系统(session management system)的一部份,确切说有两种情况下SIGHUP会在一个进程死时发送出:* 当一个终端设备与一个会话相关联,而这个会话的会话首领进程死时,SIGHUP被发送至这个终端设备的所有前台进程组。* 当一个进程死去导致一个进程组变成孤儿
23、,而且该进程组里一个或多个进程处于*暂停*状态时,那么SIGHUP和SIGCONT被发送至这个孤儿进程组的所有成员进程。(一个孤儿进程组是指在该进程组中没有一个成员进程的父进程属于和该进程组相同的会话的其它进程组。)7.6 我怎样杀死一个进程的所有派生进程?没有一个完全普遍的方法来做这个。虽然你可以通过处理ps的输出确定进程间的相互关系,但因为它只表示系统的一瞬间的状态(snapshot)所以并不可靠。但是,如果你启动一个子进程,而它可能生成它自己的子进程,而你意图一次杀死整个生成的事务(job),解决方法是将最先启动的子进程置于一个新的进程组,当你需要时杀死整个进程组。建议为创建进程组而使用
24、的函数是setpgid()。在可能情况下,使用这个函数而不使用setpgrp(),因为后一个在不同系统中有所不同(在一些系统上setgrp();等同于setpgid(0,0);,在其它系统上,setpgrp()和setpgid()相同)。参见范例章节的事务-控制范例程序。放置一个子进程于其自身的进程组有一些影响。特别的,除非你显式地将该进程组放置于前台,它将被认为是一个后台事务并具有以下结果:* 试图从终端读取的进程将被SIGTTIN信号暂停。* 如果设置终端模式tostop,那么试图向终端写的进程将被SIGTTOU信号暂停。(试图改变终端模式也导致这个结果,且不管当前tostop是否设置)* 子进程将不会收到从终端发出的键盘信号(比如SIGINT或SIGQUIT)在很多应用程序中输入和输出总会被重定向,所以最显著的影响将是丧失键盘信号。父进程需要安排程序起码捕获SIGINIT和SIGQUIT(可能情况下,还有SIGTERM),并在需要情况下清除后台事务。
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1