Linux的精灵(daemon)进程和僵尸(zombie)进程

有些日子没有跟工程师一起工作了。昨天星期五,我回到川崎的办公室和同事一起结果一个客户反馈的问题。这个工作却又让我怀念以前作研发的那些时光。突然觉得有些东西还是不应该被忘记,就写在这里作笔记了。

守护进程(Daemon)

守护进程——我情愿把他叫做精灵进程——是多种操作系统中的一种常驻程序。常驻的意思是,在系统运行期间,这些进程都一直存在。很明显,大多数服务器程序都是精灵进程。在这里我要说一说自己知道关于精灵进程的方法,和与此相关的一些技巧。

如何产生一个精灵进程?

Linux系统启动以后,它通常启动的第一个进程就是init进程。init进程的进程ID是1,并且它会一直在系统运行期间存续。同时,Linux对进程的管理策略有两个比较特殊的地方,其中之一就是,当一个进程的父进程结束,它的子进程如果还继续运行,那么,子进程就会被init进程收养。也算就是说,Linux对进程的管理策略,其实是一直在努力维护一棵树。利用这一点,就可产生伴随init一直存在的精灵进程。代码例子如下:

/* file: daemon_sample.c */
#include 
#include 
#include 
#include 

int main ()
{
	pid_t	pid,sid;

	/* fork to leave parent */
	pid = fork ();
	if (pid < 0) {
		printf ("failed to fork!n");
		exit (EXIT_FAILURE);
	} else if (pid > 0) {
		/* parent process go to exit */
		exit (EXIT_SUCCESS);
	}

	/* child process */
	chdir("/");
	sid = setsid ();
	if (sid < 0) {
		printf ("Failed to set session id.n");
		exit (EXIT_FAILURE);
	}
	/* ensure all files created by itself are accessible */
	umask (0);

	printf ("Daemon adopted by parent %dn", getppid());

	/* daemon loop */
	while (1) {
		sleep (1);
	}
}

产生一个精灵进程的过程需要有一下几步:

  1. 从启动该程序的进程中fork出来一个子进程;如果子进程产生成功,那么就将父进程结束
  2. 子进程需要作一些处理来保证自己作为精灵的地位;改变当前工作目录;利用setsid系统调用重新设置一个session;利用umask系统调用消除父进程对于文件控制的影响,从而保证精灵可以正确的访问它自己生成的各种文件——其中比较重要的是IPC
  3. 子进程需要启动一个自身处理的循环

这是一个非常简单的例子,通常情况下,子进程还需要关闭标准输入输出等等工作。与此相关的详细讨论可以查看
http://www.netzmafia.de/skripten/unix/linux-daemon-howto.html
这里有一个可以下载到本地的PDF版本
http://www.google.co.jp/url?sa=t&source=web&ct=res&cd=2&url=http%3A%2F%2Fwww.cs.aau.dk%2F~adavid%2Fteaching%2FMTP-05%2Fexercises%2F10%2FLinux-Daemon_Writting.pdf&ei=W7pFSreDHoTW7APC0_Eh&usg=AFQjCNHyZnXmdKL8ebxJ8UXQsYeGHiqRwQ&sig2=7Of2PvUnmTm8xqZifI-aAw

怎样判断一个进程是否是精灵进程?

恩,奇怪的地方就在与此了。如果用如下命令编译上面的简短程序,你会发现一处不太符合想象的地方。

$ gcc -o daemon_sample daemon_sample.c

在终端窗口中运行这个程序,然后我们作一些观察。

$ ./daemon_sample

按照上面我叙述过的内容,这个程序的将被init进程收养。那么,理所当然的它的父进程ID应该是“1”。然而,这个程序运行时却抛出了这样一句话。

Daemon adopted by parent 29914

这里的29914是通过getppid()取得的,难道有什么不对头?为了确认情况,再用ps查看一下究竟发生了什么事情。

$ ps -e -o pid,ppid,cmd,tty | grep daemon_sample
29915 1 ./daemon_sample ?

利用ps命令的-o选项,可以查看进程的pid,ppid,命令行和运行终端的信息。显而易见,这个精灵进程事实上的ppid是“1”。那么,为什么getppid()不能返回正确的值?难道这里是个BUG?就目前Linux内核的状况来说,这不是一个BUG,查看getppid的手册页可以看到,getppid并不保证能够取得一个进程被收养以后的父进程ID。或者有兴趣的xdjm可以尝试一下将这个功能升级一下:)。

换句话说,不能使用getppid来判断一个进程时候是精灵进程。那么,应该怎样判断?

再看上面ps的输出结果,你会发现这个进程的tty域是一个问号。这表明,该进程并没有被attach到任何终端设备上去。事实上所有的精灵进程都有这个特性。利用ps做一个验证吧。

$ ps -e -o pid,ppid,cmd,tty | grep daemon
898 1 /sbin/udevd --daemon ?
2464 1 /bin/dbus-daemon --system ?
3305 1 avahi-daemon: running [capr ?
3829 1 //bin/dbus-daemon --fork -- ?
29915 1 ./daemon_sample ?
31584 29871 grep --color=auto daemon pts/1

这里有ps命令的结果,它被attach到伪终端pts/1。所以,可以利用这个特性来判断一个进程是否是精灵进程。

if ((devtty = open ("/dev/tty", O_RDWD)) < 0) {
   printf ("This is a daemonn");
} else {
   printf ("This is not a daemonn");
}

在UNIX论坛上有一个关于这个话题的旧帖子,地址如下:
http://www.unix.com/high-level-programming/72479-how-find-if-process-daemon.html
精灵进程大致就是这样。

僵尸进程

僵尸进程其实是个错误。

这个错误产生的原因在于,子进程已经结束,但是父进程却没有对它进行清理。僵尸进程会被init收编,并且init会料理这些僵尸的“善后事宜”。我们可以写一个很蹩脚的程序来产生一个货真价实的僵尸进程。僵尸来了!

/* file: zombie.c */
#include 
#include 
#include 

int main ()
{
	pid_t child_pid;

	/* Create a child process. */
	child_pid = fork ();
	if (child_pid > 0) {
	   /* This is the parent process. Sleep for a minitue. */
	   sleep (60);
	} else {
	   /* This is the child process. Exit immediately. */
	   exit (EXIT_SUCCESS);
	}
	return 0;
}

编译并运行这个程序,在程序运行期间,利用ps查看它的状态,你会得到下面的结果:

$ ps -e -o pid,stat,cmd | grep zombie
4302 S+ ./zombie
4303 Z+ [zombie]

你会发现进程4302——也就是僵尸的父进程——还没有结束,而进程4303进程的状态已经变成了“Z”。这也就意味这该程序产生的子进程已经变成了僵尸。

在一般的桌面系统上,即使产生僵尸进程也不是什么大不了的事情,因为,系统资源并不紧张的情况下,僵尸最终会被init清理。但是,在高负载的服务器或者资源非常有限的嵌入式系统中如果大量出现僵尸,那会是个一个麻烦。所以,避免产生僵尸进程的方法就是在父进程中正确的处理wait和SIGCHLD信号,具体做法google一下可以出很多结果,这里就不再累述了。

打造嵌入式软件开发团队(1) 从环境配置开始 Ubuntu + NFS服务器 文件共享

嵌入式Linux开发环境的构成

+------------------+                 +----------------+
|                  |    Serial       |                |
|    (TARGET)      |<--------------->|     (HOST)     |
|   Development    |                 |   Workstation  |
|      Board       |                 |                |
|                  |<--------------->|                |
|                  |   Ethernet      |                |
+------------------+                 +----------------+

一个典型的嵌入式Linux开发环境通常长得有点像上面这张图。开发用的主机通过串口连接(Serial Connection)或者以太网连接(Ethernet Connection)和开发板进行通讯。这个通讯可能包括对开发板进行控制或者在开发板和主机之间传递数据和文件。行内黑话将开发板叫做TARGET,而将开发用的电脑叫做HOST。因为TARGET和HOST之间可以采取一些工具进行通讯,为了方便调试,开发人员通常将正在开发的内核和根文件系统都放在HOST上,在TARGET启动时由HOST下载到TARGET执行这种方式构成一个基本的小网络。这篇文章就说一下怎样利用debian base的Linux操作系统建立这样的开发环境。

为什么使用Debian

使用debian base的系统作为嵌入式软件开发平台的好处在于,它们提供了丰富的软件包。每个Linux发行版(Distro)本都有自己忠实的用户,所以,这里不需要口水战,我只是陈述事实。如果你真的有兴趣选择一下发行版比较一下吧。RedHat系发行版代表Fedora—— 在官方支持的软件频道当中支持7334个软件包(2007年数据)。而Debian软件频道当中有23851个软件包(2007年数据)。适合桌面系统的Ubuntu发行版中比Debian更多一些,有24088个软件包。

嵌入式软件开发是一个比较复杂的过程,所以,你可能在不同层面上需要工具支持。小到代码统计,查找,大到成套的调试和测试系统,还有处理硬件的flash烧制,硬件检测等等Debian或者Ubuntu几乎都可以找到合适的工具使用。众所周知,欲善其事必先利其器,选择一个合适自己的工作站和操作系统是相当重要的事情。除非你的团队依附与某些只能运行在Windows平台的商业软件,Windows工作站基本不能适合用来作嵌入式Linux系统的开发,原因众多,“罄竹难书”。

此外,Debian base的系统都采用apt作为软件发布工具,这是Debian又一个强有力的工具。apt可以帮助你的工作站一直保持更新状态。事实上,像apt这样的软件分发方式才更加自然更加贴近人们对软件的需要。如果,你使用Debian有已经成为习惯,那么毋庸置疑你对Windows世界的商业软件分发方式肯定是深恶痛绝了。无论哪个软件包,用户需要做的只是

apt-get install whatever

就可以将最新版本的软件安装到工作站。所以,对于一个需求众多又复杂的开发环境来说,Debian再合适不过了。

使用NFS挂载根文件系统

什么是NFS

Network File System的简称。NFS可以通过网络在类UNIX系统之间进行文件共享。通过NFS,你可以像使用本地文件系统一样使用远程文件系统。最初,NFS就在最近成为历史的Sun(太阳,升阳)公司开发的。

必要的软件包


$ sudo apt-get install portmap nfs-common nfs-kernel-server

就NFS服务器来说Ubuntu 8.04中提供两个版本,一个是使用的kernel服务的版本,另外一个是通常的daemon版本。Kernel服务版本的NFS使用了Kernel Thread,所以,性能比较好,同时可以使用文件锁定功能。nfs-user-server是通常的daemon版本,虽然速度比nfs-kernel-server要弱一些,但是,却可以选择更多的配置。

这些软件报安装之后会在操作系统上安装系统服务,可以用下面的命令确认。

$ sysv-rc-conf --list | grep -E 'nfs|portmap'
nfs-common 0:off 1:off 2:on 3:on 4:on 5:on 6:off S:on
nfs-kernel-s 0:off 1:off 2:on 3:on 4:on 5:on 6:off
portmap 0:on 1:off 2:on 3:on 4:on 5:on 6:on S:on

sysv-rc-conf是一个系统服务管理程序,如果系统中没有这个命令的话,Ubuntu会提示你使用apt-get安装,照做就是了。如果这里显示的是nfs-kernel-s的话,那么说明Kernel服务版本的NFS服务器安装成功。如果你仅仅着把NFS服务配置成功,那么一些小节请直接跳过。这些都是为那些有时间并且愿意知其然和其所以然的人准备的。

启动顺序

在Ubuntu系统上,这些服务会按照软件包事先定制好的顺序安装和登录并且按照顺序启动。这里需要说明的是这些服务的启动顺序,以便于检查可能出现的错误,以及避免手动安装时的手足无措。NFS服务器关联这三个服务分别是portmap->nfslock->nfs,起启动顺序也应该如上面说的那样,但是因为在Ubuntu系统中这些服务都被包装成了不同的名字,那么,相应的正确的启动顺序应该是portmap->nfs-common->nfs-kernel-server。查看/etc/rc3.d文件夹的内容可以确认各个服务的启动顺序。

$ cd /etc/rc3.d
$ ls *{portmap,nfs}*
S17portmap S20nfs-common S20nfs-kernel-server

应为NFS服务事实上是以RPC为基础的,所以,nfs-common和nfs-kernel-server依赖于portmap也是理所当然的事情咯。

RPC

portmap是用来管理Linux中的RPC(Remote Procedure Call)远程过程调用的工具。NFS服务也是通过RPC提供的。RPC提供了一种机制,它帮助计算机在本地调用另外一台远程计算机上运行的服务。不同RPC服务通过不同的程序号识别,这些号码都可以在/etc/rpc配置文件中设定。

...
portmapper 100000 portmap sunrpc
rstatd 100001 rstat rstat_svc rup perfmeter
rusersd 100002 rusers
nfs 100003 nfsprog
...

RPC和portmap

通过RPC机制提供的服务需要在运行时得到TCP/IP网络的端口地址。管理分配这些端口地址的服务就是portmap。它会根据客户端发送来的服务号码提供相应的端口地址。为了确认现在已经登录的RPC程序,你可以用
rpcinfo -p

命令来查看结果。

$ /etc/rc3.d$ rpcinfo -p
program vers proto port
100000 2 tcp 111 portmapper
100000 2 udp 111 portmapper
100024 1 udp 36151 status
100024 1 tcp 47201 status
100003 2 udp 2049 nfs
100003 3 udp 2049 nfs
100003 4 udp 2049 nfs
100021 1 udp 41471 nlockmgr
100021 3 udp 41471 nlockmgr
100021 4 udp 41471 nlockmgr
100003 2 tcp 2049 nfs
100003 3 tcp 2049 nfs
100003 4 tcp 2049 nfs
100021 1 tcp 59457 nlockmgr
100021 3 tcp 59457 nlockmgr
100021 4 tcp 59457 nlockmgr
100005 1 udp 59376 mountd
100005 1 tcp 48343 mountd
100005 2 udp 59376 mountd
100005 2 tcp 48343 mountd
100005 3 udp 59376 mountd
100005 3 tcp 48343 mountd

从上面这个结果中可以看到,nfs的TCP/IP端口号码是2049,而portmap的端口号是111。客户端通过端口111和NFS服务所在的主机交流,并且提供它需要的服务的程序号码,这里是100003;portmap就会告诉客户端,nfs服务运行在2049端口。接下来,客户端就可以利用2049这个端口和NFS服务器进行通信了。事实上大多数的服务器上NFS服务都运行在2049端口。

portmpa的访问控制

因为portmap中链接了TCPWrappers的libwrap库,所以,可以通过其运行主机上的/etc/hosts.allow和/etc/hosts.deny对它进行访问控制。这两个配置文件相对简单,下面给出的例子,正和适合在局域网中使用portmap和nfs。但是,这里需要注意的是/etc/hosts.allow和/etc/hosts.deny这两个配置文件的改变会影响到所有的以RPC为基础的服务。

认可所有来自192.168.1.*的访问

## file /etc/hosts.allow
ALL:127.0.0.1
portmap:192.168.1.
lockd:192.168.1.
mountd:192.168.1.
statd:192.168.1.

拒绝所有其他主机的访问

## file /etc/hosts.deny
portmap:ALL
lockd:ALL
mountd:ALL
rquotad:ALL
statd:ALL

设置共享目录

NFS共享目录可以通过改变/etc/exports文件来设置。这个文件中的每一行设置一个共享目录,并且给共享目录加上相应的访问控制和其他属性。基本的格式如下:

<需要共享的目录> <可以访问该目录的主机>(<用逗号分隔的选项>)

主要的选项列表

标志 意义
ro 只读许可
rw 读写许可
root_squash 将客户端的root用户作为本机的nobody用户对待。即使不指定这个选项,该选项也是有效的。如果没有特别的理由,就不要打开它。
no_root_squash 将客户端的root用户作为本机的root用户对待。除非你不得不这样作,并且清楚的知道这样作会面临什么风险,否则不要随便使用这个选项
all_squash 将所有的远程用户当作nobody对待
anonuid 指定这个选项将使远程用户作为相应的本地用户对待,并且给匿名用户指定一个UID
anongid 于上一个选项大同小异,这次指定的是GID
no_subtree_check 不检查共享文件的子文件树

相关这些选项更详细的介绍,还是man exports来的快一些。

嵌入式Linux软件开发环境中,经常使用NFS服务提供一个可以很方便的进行调试和更改的根文件系统(root filesystem)。比较典型的设置如下所示:

/export 192.168.1.0/24(rw,no_root_squash,no_subtree_check,insecure,sync)

这里设置共享了/export目录。并且,这个目录的整个子目录树都是对外可见的,通常,正在开发中的根文件系统将被放在/export/rootdisk这个位置。这个设置中,客户端的root用户将被作为服务器断的root用户对待,原因在于,很多嵌入式Linux系统需要root初始化设备或者配置系统,这样的话就需要有权限更改相应的目录和文件。这样做之后,有形成安全漏洞的可能性,所有在访问控制的文件中务必设置相应的安全管理机制。

反映共享目录的变更

每次/etc/exports文件变更以后,需要重新启动服务反映最新的更改。可以采用下面的方式,

$ sudo service nfs-kernel-server reload
* Re-exporting directories for NFS kernel daemon... [ OK ]

或者

$ sudo /etc/init.d/nfs-common restart
* Stopping NFS common utilities [ OK ]
* Starting NFS common utilities [ OK ]
$ sudo /etc/init.d/nfs-kernel-server restart
* Stopping NFS kernel daemon [ OK ]
* Unexporting directories for NFS kernel daemon... [ OK ]
* Exporting directories for NFS kernel daemon... [ OK ]
* Starting NFS kernel daemon [ OK ]

或者

$ sudo exportfs -r

都可以达成这个目标。

确认共享目录

nfs-common软件包中包含的showmount命令可以用来确认某台主机上正在被共享的文件夹。-e选项可以用来指定主机名。这样的话,即使你在客户端可以用这个命令确认希望连接到的服务器端的共享目录。

$ showmount -e localhost
Export list for localhost:
/export 192.168.1.*

客户端的设置或者挂在根文件系统的方法

嵌入式Linxu开发的场景中,放在/export/rootdisk/中的根文件系统会被kernel在系统启动时mount。查看内核编译选项,一定要保证fs中编译了NFS文件系统支持。

File systems —>
  Network File Systems —>
    <*> NFS file system support
    [*]  Provide NFSv3 client support
    [*]   Provide client support for the NFSv3 ACL protocol extension

而在启动kernel的时候,通过bootloader向kernel传递挂在根文件系统的参数,这个命令长得有些像下面这个样子:

vmlinux 'root=/dev/nfs nfsroot=192.168.1.1:/export/rootdisk panic=5'

root选项通常被用来传递根文件系统参数,这里使用”root=/dev/nfs”指明将使用NFS挂在根文件系统,其后的”nfsroot=:“给出了真正的根文件系统目录。”panic=5″参数仅仅是指名kernel在启动过程中发生任何错误就会在5秒种内尝试重启。

天色以晚,trouble shotting改天再写。好累阿,睡觉去了。


本文在发表后被jcadam修改,添加如下内容:
参考URL:
http://itmst.blog71.fc2.com/blog-entry-89.html
开发环境的bmp图更改为ASCII图。

在Google group开设”嵌入式Linux”新闻组

今天突发奇想的在Google group搜索”嵌入式Linux”,发现居然还没有人创建这个新闻组。于是,很自然的我就当仁不让了。

人是很奇怪的动物,即便是在我建立这个新闻组不到6小时,但是已经乐在其中了。这也许就是opensource运动中motivation的核心——人不仅仅因为得到钱才开心。

废话不多说了,如果你对嵌入式软件开发,特别是嵌入式Linux开发有兴趣的话,欢迎加入我们。

Google 网上论坛

订阅 嵌入式Linux

电子邮件:

访问此论坛