涉及到通过系统调用对进程的创建控制以及进程间通讯的实现。
Now check this out first.
Linux
系统调用
--fork
函数详解
功能描述:
建立一个子进程。所建立的子进程
PID
和
PPID
不同于其父进程,同时资源使用被设置为
0
,文件锁和挂起的信号不被继承。
Linux
内部,
fork
的执行使用
copy-on-write
页面,所以耗费的资源只是拷贝父进程页表,建立唯一子进程
task
结构体的时间和内存。
用法:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回说明:
成功执行时,在父进程的线程执行中返回子进程
PID
,在子进程的线程执行中返回
0
。失败时在父进程的语境中返回
-1
,
errno
被设为以下的某个值
EAGAIN
:
1.
不能为拷贝父进程的页表和子进程的
task
结构分配足够的内存
2.
不可能再建立新的子进程,因为调用者已达到
RLIMIT_NPROC
的资源限制,除了调用进程有
CAP_SYS_ADMIN
或
CAP_SYS_RESOURCE
的权限
ENOMEM
:内核内存不足,无法分配必须的数据结构
例子:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
pid_t pid;
printf("Before fork ...\n");
switch(pid = fork()) {
case -1:
printf("Fock call fail\n");
exit(1);
case 0:
printf("The pid of child is: %d\n", getpid());
printf("The pid of child's parent is: %d\n", getppid());
printf("Child exiting...\n");
exit(0);
default:
printf("The pid of parent is: %d\n", getpid());
printf("the pid of parent's child is: %d\n", pid);
}
printf("After fork, program exiting...\n");
exit(0);
}
unix/linux
管道通信相关函数
程序中有如下
Linux
系统调用
:
(1) fork( ):
创建一个进程。
返回值:
-1
:创建子进程失败。
0
:子进程得到的返回值。
>0
:父进程得到的返回值,表示子进程号(
pid)
。
创建子进程后,父、子进程执行同一个程序段,但有不同的数据区,子进程继
承父进程的资源。
(2) exit( ):
撤消进程。
若父进程因
wait()
而睡眠,则唤醒父进程。
exit()
撤消子进程时必须通知父
进程。
(3) pipe(fd):
创建一个管道
. fd[0]
为管道的读端
,fd[1]
为管道的写端。管道用来实现父进程与其子孙进程
之间的通信
,
以
FIFO
方式传送消息。
(4) wait():
父进程等待子进程撤销。
若子进程尚未撤销
,
父进程睡眠等待
,
子进程撤销时
,
将其唤醒,若子进程已撤
销,父进程不睡眠等待。
)
(5) Sleep(n):
进程睡眠等待
n
秒,交出处理机控制权。
/*----------------------------------------------------*/
下面我引一篇在网上找到的文章,对理清概念很有帮助,并且里面提到几个很重要点的。
我将用红色注明。
/*----------------------------------------------------*/
进程管理相关的系统调用
本文介绍了
Linux
下的进程概念,并着重讲解了与
Linux
进程管理相关的
4
个重要系统调用
getpid,fork,exit
和
_exit
,辅助一些例程说明了它们的特点和使用方法。
关于进程的一些必要知识
先看一下进程在大学课本里的标准定义:“进程是可并发执行的程序在一个数据集合上的运行过程。”这个定义非常严谨,而且难懂,如果你没有一下子理解这句话,就不妨看看笔者自己的并不严谨的解释。我们大家都知道,硬盘上的一个可执行文件经常被称作程序,在
Linux
系统中,当一个程序开始执行后,在开始执行到执行完毕退出这段时间里,它在内存中的部分就被称作一个进程。
当然,这个解释并不完善,但好处是容易理解,在以下的文章中,我们将会对进程作一些更全面的认识。
Linux
进程简介
Linux
是一个多任务的操作系统,也就是说,在同一个时间内,可以有多个进程同时执行。如果读者对计算机硬件体系有一定了解的话,会知道我们大家常用的单
CPU
计算机实际上在一个时间片断内只能执行一条指令,那么
Linux
是如何实现多进程同时执行的呢?原来
Linux
使用了一种称为“进程调度(
process scheduling
)”的手段,首先,为每个进程指派一定的运行时间,这个时间通常很短,短到以毫秒为单位,然后依照某种规则,从众多进程中挑选一个投入运行,其他的进程暂时等待,当正在运行的那个进程时间耗尽,或执行完毕退出,或因某种原因暂停,
Linux
就会重新进行调度,挑选下一个进程投入运行。因为每个进程占用的时间片都很短,在我们使用者的角度来看,就好像多个进程同时运行一样了。
在
Linux
中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(
Process Control Block
,简称
PCB
)。
PCB
中包含了很多重要的信息,供系统调度和进程本身执行使用,其中最重要的莫过于进程
ID
(
process ID
)了,进程
ID
也被称作进程标识符,是一个非负的整数,在
Linux
操作系统中唯一地标志一个进程,在我们最常使用的
I386
架构(即
PC
使用的架构)上,一个非负的整数的变化范围是
0-32767
,这也是我们所有可能取到的进程
ID
。其实从进程
ID
的名字就可以看出,它就是进程的身份证号码,每个人的身份证号码都不会相同,每个进程的进程
ID
也不会相同。
一个或多个进程可以合起来构成一个进程组(
process group
),一个或多个进程组可以合起来构成一个会话(
session
)。这样我们就有了对进程进行批量操作的能力,比如通过向某个进程组发送信号来实现向该组中的每个进程发送信号。
最后,让我们通过
ps
命令亲眼看一看自己的系统中目前有多少进程在运行:
$ps -aux (以下是在我的计算机上的运行结果,你的结果很可能与这不同。) USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.1 0.4 1412 520 ? S May15 0:04 init [3] root 2 0.0 0.0 0 0 ? SW May15 0:00 [keventd] root 3 0.0 0.0 0 0 ? SW May15 0:00 [kapm-idled] root 4 0.0 0.0 0 0 ? SWN May15 0:00 [ksoftirqd_CPU0] root 5 0.0 0.0 0 0 ? SW May15 0:00 [kswapd] root 6 0.0 0.0 0 0 ? SW May15 0:00 [kreclaimd] root 7 0.0 0.0 0 0 ? SW May15 0:00 [bdflush] root 8 0.0 0.0 0 0 ? SW May15 0:00 [kupdated] root 9 0.0 0.0 0 0 ? SW< May15 0:00 [mdrecoveryd] root 13 0.0 0.0 0 0 ? SW May15 0:00 [kjournald] root 132 0.0 0.0 0 0 ? SW May15 0:00 [kjournald] root 673 0.0 0.4 1472 592 ? S May15 0:00 syslogd -m 0 root 678 0.0 0.8 2084 1116 ? S May15 0:00 klogd -2 rpc 698 0.0 0.4 1552 588 ? S May15 0:00 portmap rpcuser 726 0.0 0.6 1596 764 ? S May15 0:00 rpc.statd root 839 0.0 0.4 1396 524 ? S May15 0:00 /usr/sbin/apmd -p root 908 0.0 0.7 2264 1000 ? S May15 0:00 xinetd -stayalive root 948 0.0 1.5 5296 1984 ? S May15 0:00 sendmail: accepti root 967 0.0 0.3 1440 484 ? S May15 0:00 gpm -t ps/2 -m /d wnn 987 0.0 2.7 4732 3440 ? S May15 0:00 /usr/bin/cserver root 1005 0.0 0.5 1584 660 ? S May15 0:00 crond wnn 1025 0.0 1.9 3720 2488 ? S May15 0:00 /usr/bin/tserver xfs 1079 0.0 2.5 4592 3216 ? S May15 0:00 xfs -droppriv -da daemon 1115 0.0 0.4 1444 568 ? S May15 0:00 /usr/sbin/atd root 1130 0.0 0.3 1384 448 tty1 S May15 0:00 /sbin/mingetty tt root 1131 0.0 0.3 1384 448 tty2 S May15 0:00 /sbin/mingetty tt root 1132 0.0 0.3 1384 448 tty3 S May15 0:00 /sbin/mingetty tt root 1133 0.0 0.3 1384 448 tty4 S May15 0:00 /sbin/mingetty tt root 1134 0.0 0.3 1384 448 tty5 S May15 0:00 /sbin/mingetty tt root 1135 0.0 0.3 1384 448 tty6 S May15 0:00 /sbin/mingetty tt root 8769 0.0 0.6 1744 812 ? S 00:08 0:00 in.telnetd: 192.1 root 8770 0.0 0.9 2336 1184 pts/0 S 00:08 0:00 login -- lei lei 8771 0.1 0.9 2432 1264 pts/0 S 00:08 0:00 -bash lei 8809 0.0 0.6 2764 808 pts/0 R 00:09 0:00 ps -aux
以上除标题外,每一行都代表一个进程。在各列中,
PID
一列代表了各进程的进程
ID
,
COMMAND
一列代表了进程的名称或在
Shell
中调用的命令行,对其他列的具体含义,我就不再作解释,有兴趣的读者可以去参考相关书籍。
getpid
在
2.4.4
版内核中,
getpid
是第
20
号系统调用,其在
Linux
函数库中的原型是:
#include<sys/types.h> /*
提供类型
pid_t
的定义
*/
#include<unistd.h> /*
提供函数的定义
*/
pid_t getpid(void);
getpid
的作用很简单,就是返回当前进程的进程
ID
,请大家看以下的例子:
/* getpid_test.c *
/#include<unistd.h>
main()
{
printf("The current process ID is %d\n",getpid());
}
细心的读者可能注意到了,这个程序的定义里并没有包含头文件
sys/types.h
,这是因为我们在程序中没有用到
pid_t
类型,
pid_t
类型即为进程
ID
的类型。事实上,在
i386
架构上(就是我们一般
PC
计算机的架构),
pid_t
类型是和
int
类型完全兼容的,我们可以用处理整形数的方法去处理
pid_t
类型的数据,比如,用
"%d"
把它打印出来。
编译并运行程序
getpid_test.c
:
$gcc getpid_test.c -o getpid_test
$./getpid_test
The current process ID is 1980
(你自己的运行结果很可能与这个数字不一样,这是很正常的。)
再运行一遍:
$./getpid_test
The current process ID is 1981
正如我们所见,尽管是同一个应用程序,每一次运行的时候,所分配的进程标识符都不相同。
fork
在
2.4.4
版内核中,
fork
是第
2
号系统调用,其在
Linux
函数库中的原型是:
#include
<sys/types.h> /*
提供类型
pid_t
的定义
*/
#include<unistd.h> /*
提供函数的定义
*/
pid_t fork(void);
只看
fork
的名字,可能难得有几个人可以猜到它是做什么用的。
fork
系统调用的作用是复制一个进程。当一个进程调用它,完成后就出现两个几乎一模一样的进程,我们也由此得到了一个新进程。据说
fork
的名字就是来源于这个与叉子的形状颇有几分相似的工作流程。
在
Linux
中,创造新进程的方法只有一个,就是我们正在介绍的
fork
。其他一些库函数,如
system()
,看起来似乎它们也能创建新的进程,如果能看一下它们的源码就会明白,它们实际上也在内部调用了
fork
。包括我们在命令行下运行应用程序,新的进程也是由
shell
调用
fork
制造出来的。
fork
有一些很有意思的特征,下面就让我们通过一个小程序来对它有更多的了解。
/* fork_test.c */
#include<sys/types.h>
#inlcude<unistd.h>
main()
{
pid_t pid; /*
此时仅有一个进程
*/
pid=fork(); /*
此时已经有两个进程在同时运行
*/
if(pid<0)
printf("error in fork!");
else if(pid==0)
printf("I am the child process, my process ID is %d\n",getpid());
else printf("I am the parent process, my process ID is %d\n",getpid());
}
编译并运行:
$gcc fork_test.c -o fork_test
$./fork_test
I am the parent process, my process ID is 1991
I am the child process, my process ID is 1992
看这个程序的时候,头脑中必须首先了解一个概念:在语句
pid=fork()
之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的代码部分完全相同,将要执行的下一条语句都是
if(pid==0)
……。
两个进程中,原先就存在的那个被称作“父进程”,新出现的那个被称作“子进程”。父子进程的区别除了进程标志符(
process ID
)不同外,变量
pid
的值也不相同,
pid
存放的是
fork
的返回值。
fork
调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
在父进程中,
fork
返回新创建子进程的进程
ID
;
在子进程中,
fork
返回
0
;
如果出现错误,
fork
返回一个负值;
fork
出错可能有两种原因:
(
1
)当前的进程数已经达到了系统规定的上限,这时
errno
的值被设置为
EAGAIN
。
(
2
)系统内存不足,这时
errno
的值被设置为
ENOMEM
。(关于
errno
的意义,请参考本系列的第一篇文章。)
fork
系统调用出错的可能性很小,而且如果出错,一般都为第一种错误。如果出现第二种错误,说明系统已经没有可分配的内存,正处于崩溃的边缘,这种情况对
Linux
来说是很罕见的。
说到这里,聪明的读者可能已经完全看懂剩下的代码了,如果
pid
小于
0
,说明出现了错误;
pid==0
,就说明
fork
返回了
0
,也就说明当前进程是子进程,就去执行
printf("I am the child!")
,否则(
else
),当前进程就是父进程,执行
printf("I am the parent!")
。完美主义者会觉得这很冗余,因为两个进程里都各有一条它们永远执行不到的语句。不必过于为此耿耿于怀,毕竟很多年以前,
UNIX
的鼻祖们在当时内存小得无法想象的计算机上就是这样写程序的,以我们如今的“海量”内存,完全可以把这几个字节的顾虑抛到九霄云外。
说到这里,可能有些读者还有疑问:如果
fork
后子进程和父进程几乎完全一样,而系统中产生新进程唯一的方法就是
fork
,那岂不是系统中所有的进程都要一模一样吗?那我们要执行新的应用程序时候怎么办呢?从对
Linux
系统的经验中,我们知道这种问题并不存在。至于采用了什么方法,我们把这个问题留到后面具体讨论。
exit
在
2.4.4
版内核中,
exit
是第
1
号调用,其在
Linux
函数库中的原型是:
#include<stdlib.h>
void exit(int status);
不像
fork
那么难理解,从
exit
的名字就能看出,这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到
exit
系统调用,进程就会停止剩下的所有操作,清除包括
PCB
在内的各种数据结构,并终止本进程的运行。请看下面的程序:
/* exit_test1.c */
#include<stdlib.h>
main()
{
printf("this process will exit!\n");
exit(0);
printf("never be displayed!\n");
}
编译后运行:
$gcc exit_test1.c -o exit_test1$./exit_test1this process will exit!
我们可以看到,程序并没有打印后面的
"never be displayed!\n"
,因为在此之前,在执行到
exit(0)
时,进程就已经终止了。
exit
系统调用带有一个整数类型的参数
status
,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,
0
表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。我们在实际编程时,可以用
wait
系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。关于
wait
的详细情况,我们将在以后的篇幅中进行介绍。
exit
和
_exit
作为系统调用而言,
_exit
和
exit
是一对孪生兄弟,它们究竟相似到什么程度,我们可以从
Linux
的源码中找到答案:
#define __NR__exit __NR_exit /*
摘自文件
include/asm-i386/unistd.h
第
334
行
*/
“
__NR_
”是在
Linux
的源码中为每个系统调用加上的前缀,请注意第一个
exit
前有
2
条下划线,第二个
exit
前只有
1
条下划线。
这时随便一个懂得
C
语言并且头脑清醒的人都会说,
_exit
和
exit
没有任何区别,但我们还要讲一下这两者之间的区别,这种区别主要体现在它们在函数库中的定义。
_exit
在
Linux
函数库中的原型是:
#include<unistd.h>
void _exit(int status);
和
exit
比较一下,
exit()
函数定义在
stdlib.h
中,而
_exit()
定义在
unistd.h
中,从名字上看,
stdlib.h
似乎比
unistd.h
高级一点,那么,它们之间到底有什么区别呢?让我们先来看流程图,通过下图,我们会对这两个系统调用的执行过程产生一个较为直观的认识。
_exit()
函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit()
函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为
exit
已经不能算是纯粹的系统调用。
exit()
函数与
_exit()
函数最大的区别就在于
exit()
函数在调用
exit
系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理
I/O
缓冲”一项。
在
Linux
的标准函数库中,有一套称作“高级
I/O
”的函数,我们熟知的
printf()
、
fopen()
、
fread()
、
fwrite()
都在此列,它们也被称作“缓冲
I/O
(
buffered I/O
)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符
\n
和文件结束符
EOF
),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用
_exit()
函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用
exit()
函数。
请看以下例程:
/* exit2.c */
#include<stdlib.h>
main()
{
printf("output begin\n");
printf("content in buffer");
exit(0);
}
编译并运行:
$gcc exit2.c -o exit2
$./exit2
output begin
content in buffer
/* _exit1.c *
/#include<unistd.h>
main()
{
printf("output begin\n");
printf("content in buffer");
_exit(0);
}
编译并运行:
$gcc _exit1.c -o _exit1$./_exit1output begin
在
Linux
中,标准输入和标准输出都是作为文件处理的,虽然是一类特殊的文件,但从程序员的角度来看,它们和硬盘上存储数据的普通文件并没有任何区别。与所有其他文件一样,它们在打开后也有自己的缓冲区。
请读者结合前面的叙述,思考一下为什么这两个程序会得出不同的结果。相信如果您理解了我前面所讲的内容,会很容易的得出结论。
1.7
背景
在前面的文章中,我们已经了解了父进程和子进程的概念,并已经掌握了系统调用
exit
的用法,但可能很少有人意识到,在一个进程调用了
exit
之后,该进程并非马上就消失掉,而是留下一个称为僵尸进程(
Zombie
)的数据结构。在
Linux
进程的
5
种状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。从这点来看,僵尸进程虽然有一个很酷的名字,但它的影响力远远抵不上那些真正的僵尸兄弟,真正的僵尸总能令人感到恐怖,而僵尸进程却除了留下一些供人凭吊的信息,对系统毫无作用。
也许读者们还对这个新概念比较好奇,那就让我们来看一眼
Linux
里的僵尸进程究竟长什么样子。
当一个进程已退出,但其父进程还没有调用系统调用
wait
(稍后介绍)对其进行收集之前的这段时间里,它会一直保持僵尸状态,利用这个特点,我们来写一个简单的小程序:
/* zombie.c */
#include <sys/types.h>
#include <unistd.h>
main()
{
pid_t pid;
pid=fork();
if(pid<0) /*
如果出错 */
printf("error occurred!\n");
else if(pid==0) /*
如果是子进程 */
exit(0);
else /*
如果是父进程 */
sleep(60); /*
休眠60秒,这段时间里,父进程什么也干不了 */
wait(NULL); /*
收集僵尸进程 */
}
|
sleep
的作用是让进程休眠指定的秒数,在这
60
秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程
60
秒的僵尸状态。
编译这个程序:
$ cc zombie.c -o zombie
|
后台运行程序,以使我们能够执行下一条命令
$ ./zombie &
[1] 1577
|
列一下系统内的进程
$ ps -ax
... ...
1177 pts/0 S 0:00 -bash
1577 pts/0 S 0:00 ./zombie
1578 pts/0 Z 0:00 [zombie <defunct>]
1579 pts/0 R 0:00 ps -ax
|
看到中间的
"Z"
了吗?那就是僵尸进程的标志,它表示
1578
号进程现在就是一个僵尸进程。
我们已经学习了系统调用
exit
,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。僵尸进程虽然对其他进程几乎没有什么影响,不占用
CPU
时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服。而且
Linux
系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。那么,我们该如何来消灭这些僵尸进程呢?
先来了解一下僵尸进程的来由,我们知道,
Linux
和
UNIX
总有着剪不断理还乱的亲缘关系,僵尸进程的概念也是从
UNIX
上继承来的,而
UNIX
的先驱们设计这个东西并非是因为闲来无聊想烦烦其他的程序员。僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统
CPU
时间和总用户
CPU
时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就只好干瞪眼了。
那么,我们如何收集这些信息,并终结这些僵尸进程呢?就要靠我们下面要讲到的
waitpid
调用和
wait
调用。这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。下面就对这两个调用分别作详细介绍。
1.8.1
简介
wait
的函数原型是:
#include <sys/types.h> /*
提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t wait(int *status)
|
进程一旦调用了
wait
,就立即阻塞自己,由
wait
自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,
wait
就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,
wait
就会一直阻塞在这里,直到有一个出现为止。
参数
status
用来保存被收集进程退出时的一些状态,它是一个指向
int
类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为
NULL
,就象下面这样:
pid = wait(NULL);
|
如果成功,
wait
会返回被收集的子进程的进程
ID
,如果调用进程没有子进程,调用就会失败,此时
wait
返回
-1
,同时
errno
被置为
ECHILD
。
1.8.2
实战
下面就让我们用一个例子来实战应用一下
wait
调用,程序中用到了系统调用
fork
,如果你对此不大熟悉或已经忘记了,请参考上一篇文章《进程管理相关的系统调用(一)》。
/* wait1.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main()
{
pid_t pc,pr;
pc=fork();
if(pc<0) /*
如果出错 */
printf("error ocurred!\n");
else if(pc==0){ /*
如果是子进程 */
printf("This is child process with pid of %d\n",getpid());
sleep(10); /*
睡眠10秒钟 */
}
else{ /*
如果是父进程 */
pr=wait(NULL); /*
在这里等待 */
printf("I catched a child process with pid of %d\n"),pr);
}
exit(0);
}
|
编译并运行
:
$ cc wait1.c -o wait1
$ ./wait1
This is child process with pid of 1508
I catched a child process with pid of 1508
|
可以明显注意到,在第
2
行结果打印出来前有
10
秒钟的等待时间,这就是我们设定的让子进程睡眠的时间,只有子进程从睡眠中苏醒过来,它才能正常退出,也就才能被父进程捕捉到。其实这里我们不管设定子进程睡眠的时间有多长,父进程都会一直等待下去,读者如果有兴趣的话,可以试着自己修改一下这个数值,看看会出现怎样的结果。
1.8.3
参数
status
如果参数
status
的值不是
NULL
,
wait
就会把子进程退出时的状态取出并存入其中,这是一个整数值(
int
),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(
macro
)来完成这项工作,下面我们来学习一下其中最常用的两个:
1
,
WIFEXITED(status)
这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
(请注意,虽然名字一样,这里的参数
status
并不同于
wait
唯一的参数
--
指向整数的指针
status
,而是那个指针所指向的整数,切记不要搞混了。)
2
,
WEXITSTATUS(status)
当
WIFEXITED
返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用
exit(5)
退出,
WEXITSTATUS(status)
就会返回
5
;如果子进程调用
exit(7)
,
WEXITSTATUS(status)
就会返回
7
。请注意,如果进程不是正常退出的,也就是说,
WIFEXITED
返回
0
,这个值就毫无意义。
下面通过例子来实战一下我们刚刚学到的内容:
/* wait2.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
main()
{
int status;
pid_t pc,pr;
pc=fork();
if(pc<0) /*
如果出错 */
printf("error ocurred!\n");
else if(pc==0){ /*
子进程 */
printf("This is child process with pid of %d.\n",getpid());
exit(3); /*
子进程返回3 */
}
else{ /*
父进程 */
pr=wait(&status);
if(WIFEXITED(status)){ /*
如果WIFEXITED返回非零值 */
printf("the child process %d exit normally.\n",pr);
printf("the return code is %d.\n",WEXITSTATUS(status));
}else /*
如果WIFEXITED返回零 */
printf("the child process %d exit abnormally.\n",pr);
}
}
|
编译并运行
:
$ cc wait2.c -o wait2
$ ./wait2
This is child process with pid of 1538.
the child process 1538 exit normally.
the return code is 3.
|
父进程准确捕捉到了子进程的返回值
3
,并把它打印了出来。
当然,处理进程退出状态的宏并不止这两个,但它们当中的绝大部分在平时的编程中很少用到,就也不在这里浪费篇幅介绍了,有兴趣的读者可以自己参阅
Linux man pages
去了解它们的用法。
1.8.4
进程同步
有时候,父进程要求子进程的运算结果进行下一步的运算,或者子进程的功能是为父进程提供了下一步执行的先决条件(如:子进程建立文件,而父进程写入数据),此时父进程就必须在某一个位置停下来,等待子进程运行结束,而如果父进程不等待而直接执行下去的话,可以想见,会出现极大的混乱。这种情况称为进程之间的同步,更准确地说,这是进程同步的一种特例。进程同步就是要协调好
2
个以上的进程,使之以安排好地次序依次执行。解决进程同步问题有更通用的方法,我们将在以后介绍,但对于我们假设的这种情况,则完全可以用
wait
系统调用简单的予以解决。请看下面这段程序:
#include <sys/types.h>
#include <sys/wait.h>
main()
{
pid_t pc, pr;
int status;
pc=fork();
if(pc<0)
printf("Error occured on forking.\n");
else if(pc==0){
/*
子进程的工作 */
exit(0);
}else{
/*
父进程的工作 */
pr=wait(&status);
/*
利用子进程的结果 */
}
}
|
这段程序只是个例子,不能真正拿来执行,但它却说明了一些问题,首先,当
fork
调用成功后,父子进程各做各的事情,但当父进程的工作告一段落,需要用到子进程的结果时,它就停下来调用
wait
,一直等到子进程运行结束,然后利用子进程的结果继续执行,这样就圆满地解决了我们提出的进程同步问题。
1.9.1
简介
waitpid
系统调用在
Linux
函数库中的原型是:
#include <sys/types.h> /*
提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)
|
从本质上讲,系统调用
waitpid
和
wait
的作用是完全相同的,但
waitpid
多出了两个可由用户控制的参数
pid
和
options
,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
从参数的名字
pid
和类型
pid_t
中就可以看出,这里需要的是一个进程
ID
。但当
pid
取不同的值时,在这里有不同的意义。
1.
pid>0
时,只等待进程
ID
等于
pid
的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,
waitpid
就会一直等下去。
2.
pid=-1
时,等待任何一个子进程退出,没有任何限制,此时
waitpid
和
wait
的作用一模一样。
3.
pid=0
时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,
waitpid
不会对它做任何理睬。
4.
pid<-1
时,等待一个指定进程组中的任何子进程,这个进程组的
ID
等于
pid
的绝对值。
options
提供了一些额外的选项来控制
waitpid
,目前在
Linux
中只支持
WNOHANG
和
WUNTRACED
两个选项,这是两个常数,可以用
"|"
运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
|
如果我们不想使用它们,也可以把
options
设为
0
,如:
ret=waitpid(-1,NULL,0);
|
如果使用了
WNOHANG
参数调用
waitpid
,即使没有子进程退出,它也会立即返回,不会像
wait
那样永远等下去。
而
WUNTRACED
参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。
看到这里,聪明的读者可能已经看出端倪了
--wait
不就是经过包装的
waitpid
吗?没错,察看
<
内核源码目录
>/include/unistd.h
文件
349-352
行就会发现以下程序段:
static inline pid_t wait(int * wait_stat)
{
return waitpid(-1,wait_stat,0);
}
|
1.9.2
返回值和错误
waitpid
的返回值比
wait
稍微复杂一些,一共有
3
种情况:
1.
当正常返回的时候,
waitpid
返回收集到的子进程的进程
ID
;
2.
如果设置了选项
WNOHANG
,而调用中
waitpid
发现没有已退出的子进程可收集,则返回
0
;
3.
如果调用中出错,则返回
-1
,这时
errno
会被设置成相应的值以指示错误所在;
当
pid
所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,
waitpid
就会出错返回,这时
errno
被设置为
ECHILD
;
/* waitpid.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
main()
{
pid_t pc, pr;
pc=fork();
if(pc<0) /*
如果fork出错 */
printf("Error occured on forking.\n");
else if(pc==0){ /*
如果是子进程 */
sleep(10); /*
睡眠10秒 */
exit(0);
}
/*
如果是父进程 */
do{
pr=waitpid(pc, NULL, WNOHANG); /*
使用了WNOHANG参数,waitpid不会在这里等待 */
if(pr==0){ /*
如果没有收集到子进程 */
printf("No child exited\n");
sleep(1);
}
}while(pr==0); /*
没有收集到子进程,就回去继续尝试 */
if(pr==pc)
printf("successfully get child %d\n", pr);
else
printf("some error occured\n");
}
|
编译并运行:
$ cc waitpid.c -o waitpid
$ ./waitpid
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
successfully get child 1526
|
父进程经过
10
次失败的尝试之后,终于收集到了退出的子进程。
因为这只是一个例子程序,不便写得太复杂,所以我们就让父进程和子进程分别睡眠了
10
秒钟和
1
秒钟,代表它们分别作了
10
秒钟和
1
秒钟的工作。父子进程都有工作要做,父进程利用工作的简短间歇察看子进程的是否退出,如退出就收集它。
|
也许有不少读者从本系列文章一推出就开始读,一直到这里还有一个很大的疑惑:既然所有新进程都是由
fork
产生的,而且由
fork
产生的子进程和父进程几乎完全一样,那岂不是意味着系统中所有的进程都应该一模一样了吗?而且,就我们的常识来说,当我们执行一个程序的时候,新产生的进程的内容应就是程序的内容才对。是我们理解错了吗?显然不是,要解决这些疑惑,就必须提到我们下面要介绍的
exec
系统调用。
1.10.1
简介
说是
exec
系统调用,实际上在
Linux
中,并不存在一个
exec()
的函数形式,
exec
指的是一组函数,一共有
6
个,分别是:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
|
其中只有
execve
是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec
函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何
Linux
下可执行的脚本文件。
与一般情况不同,
exec
函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程
ID
等一些表面上的信息仍保持原样,颇有些神似
"
三十六计
"
中的
"
金蝉脱壳
"
。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个
-1
,从原程序的调用点接着往下执行。
现在我们应该明白了,
Linux
下是如何执行新程序的,每当有进程认为自己不能为系统和拥护做出任何贡献了,他就可以发挥最后一点余热,调用任何一个
exec
,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以
fork
出一个新进程,然后调用任何一个
exec
,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于
Linux
专门为其作了优化,我们已经知道,
fork
会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果
fork
完之后我们马上就调用
exec
,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种
"
写时拷贝(
copy-on-write
)
"
技术,使得
fork
结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是
exec
,它就不会白白作无用功了,也就提高了效率。
1.10.2
稍稍深入
上面
6
条函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。在学习它们之前,先来了解一下我们习以为常的
main
函数。
下面这个
main
函数的形式可能有些出乎我们的意料:
int main(int argc, char *argv[], char *envp[])
|
它可能与绝大多数教科书上描述的都不一样,但实际上,这才是
main
函数真正完整的形式。
参数
argc
指出了运行该程序时命令行参数的个数,数组
argv
存放了所有的命令行参数,数组
envp
存放了所有的环境变量。环境变量指的是一组值,从用户登录后就一直存在,很多应用程序需要依靠它来确定系统的一些细节,我们最常见的环境变量是
PATH
,它指出了应到哪里去搜索应用程序,如
/bin
;
HOME
也是比较常见的环境变量,它指出了我们在系统中的个人目录。环境变量一般以字符串
"XXX=xxx"
的形式存在,
XXX
表示变量名,
xxx
表示变量的值。
值得一提的是,
argv
数组和
envp
数组存放的都是指向字符串的指针,这两个数组都以一个
NULL
元素表示数组的结尾。
我们可以通过以下这个程序来观看传到
argc
、
argv
和
envp
里的都是什么东西:
/* main.c */
int main(int argc, char *argv[], char *envp[])
{
printf("\n### ARGC ###\n%d\n", argc);
printf("\n### ARGV ###\n");
while(*argv)
printf("%s\n", *(argv++));
printf("\n### ENVP ###\n");
while(*envp)
printf("%s\n", *(envp++));
return 0;
}
|
编译它:
$ cc main.c -o main
|
运行时,我们故意加几个没有任何作用的命令行参数:
$ ./main -xx 000
### ARGC ###
3
### ARGV ###
./main
-xx
000
### ENVP ###
PWD=/home/lei
REMOTEHOST=dt.laser.com
HOSTNAME=localhost.localdomain
QTDIR=/usr/lib/qt-2.3.1
LESSOPEN=|/usr/bin/lesspipe.sh %s
KDEDIR=/usr
USER=lei
LS_COLORS=
MACHTYPE=i386-redhat-linux-gnu
MAIL=/var/spool/mail/lei
INPUTRC=/etc/inputrc
LANG=en_US
LOGNAME=lei
SHLVL=1
SHELL=/bin/bash
HOSTTYPE=i386
OSTYPE=linux-gnu
HISTSIZE=1000
TERM=ansi
HOME=/home/lei
PATH=/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/home/lei/bin
_=./main
|
我们看到,程序将
"./main"
作为第
1
个命令行参数,所以我们一共有
3
个命令行参数。这可能与大家平时习惯的说法有些不同,小心不要搞错了。
现在回过头来看一下
exec
函数族,先把注意力集中在
execve
上:
int execve(const char *path, char *const argv[], char *const envp[]);
|
对比一下
main
函数的完整形式,看出问题了吗?是的,这两个函数里的
argv
和
envp
是完全一一对应的关系。
execve
第
1
个参数
path
是被执行应用程序的完整路径,第
2
个参数
argv
就是传给被执行应用程序的命令行参数,第
3
个参数
envp
是传给被执行应用程序的环境变量。
留心看一下这
6
个函数还可以发现,前
3
个函数都是以
execl
开头的,后
3
个都是以
execv
开头的,它们的区别在于,
execv
开头的函数是以
"char *argv[]"
这样的形式传递命令行参数,而
execl
开头的函数采用了我们更容易习惯的方式,把参数一个一个列出来,然后以一个
NULL
表示结束。这里的
NULL
的作用和
argv
数组里的
NULL
作用是一样的。
在全部
6
个函数中,只有
execle
和
execve
使用了
char *envp[]
传递环境变量,其它的
4
个函数都没有这个参数,这并不意味着它们不传递环境变量,这
4
个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而
execle
和
execve
会用指定的环境变量去替代默认的那些。
还有
2
个以
p
结尾的函数
execlp
和
execvp
,咋看起来,它们和
execl
与
execv
的差别很小,事实也确是如此,除
execlp
和
execvp
之外的
4
个函数都要求,它们的第
1
个参数
path
必须是一个完整的路径,如
"/bin/ls"
;而
execlp
和
execvp
的第
1
个参数
file
可以简单到仅仅是一个文件名,如
"ls"
,这两个函数可以自动到环境变量
PATH
制定的目录里去寻找。
1.10.3
实战
知识介绍得差不多了,接下来我们看看实际的应用:
/* exec.c */
#include <unistd.h>
main()
{
char *envp[]={"PATH=/tmp",
"USER=lei",
"STATUS=testing",
NULL};
char *argv_execv[]={"echo", "excuted by execv", NULL};
char *argv_execvp[]={"echo", "executed by execvp", NULL};
char *argv_execve[]={"env", NULL};
if(fork()==0)
if(execl("/bin/echo", "echo", "executed by execl", NULL)<0)
perror("Err on execl");
if(fork()==0)
if(execlp("echo", "echo", "executed by execlp", NULL)<0)
perror("Err on execlp");
if(fork()==0)
if(execle("/usr/bin/env", "env", NULL, envp)<0)
perror("Err on execle");
if(fork()==0)
if(execv("/bin/echo", argv_execv)<0)
perror("Err on execv");
if(fork()==0)
if(execvp("echo", argv_execvp)<0)
perror("Err on execvp");
if(fork()==0)
if(execve("/usr/bin/env", argv_execve, envp)<0)
perror("Err on execve");
}
|
程序里调用了
2
个
Linux
常用的系统命令,
echo
和
env
。
echo
会把后面跟的命令行参数原封不动的打印出来,
env
用来列出所有环境变量。
由于各个子进程执行的顺序无法控制,所以有可能出现一个比较混乱的输出
--
各子进程打印的结果交杂在一起,而不是严格按照程序中列出的次序。
编译并运行:
$ cc exec.c -o exec
$ ./exec
executed by execl
PATH=/tmp
USER=lei
STATUS=testing
executed by execlp
excuted by execv
executed by execvp
PATH=/tmp
USER=lei
STATUS=testing
|
果然不出所料,
execle
输出的结果跑到了
execlp
前面。
大家在平时的编程中,如果用到了
exec
函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,
exec
很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:
1.
找不到文件或路径,此时
errno
被设置为
ENOENT
;
2.
数组
argv
和
envp
忘记用
NULL
结束,此时
errno
被设置为
EFAULT
;
3.
没有对要执行文件的运行权限,此时
errno
被设置为
EACCES
。
1.11
进程的一生
下面就让我用一些形象的比喻,来对进程短暂的一生作一个小小的总结:
随着一句
fork
,一个新进程呱呱落地,但它这时只是老进程的一个克隆。
然后随着
exec
,新进程脱胎换骨,离家独立,开始了为人民服务的职业生涯。
人有生老病死,进程也一样,它可以是自然死亡,即运行到
main
函数的最后一个
"}"
,从容地离我们而去;也可以是自杀,自杀有
2
种方式,一种是调用
exit
函数,一种是在
main
函数内使用
return
,无论哪一种方式,它都可以留下遗书,放在返回值里保留下来;它还甚至能可被谋杀,被其它进程通过另外一些方式结束他的生命。
进程死掉以后,会留下一具僵尸,
wait
和
waitpid
充当了殓尸工,把僵尸推去火化,使其最终归于无形。
这就是进程完整的一生。
1.12
小结
本文重点介绍了系统调用
wait
、
waitpid
和
exec
函数族,对与进程管理相关的系统调用的介绍就在这里告一段落,在下一篇文章,也是与进程管理相关的系统调用的最后一篇文章中,我们会通过两个很酷的实际例子,来重温一下最近学过的知识。
1.13 Shell
对
Linux
不是太陌生的读者都应该对
Shell
有一定的了解,就是这个程序在我们登陆后自动执行,打印出一个
$
符号,然后等待我们输入命令。
Linux
下最常用的
Shell
应用程序是
Bash
,绝大部分
Linux
发行版默认安装的都是它。下面我们也来亲手编写一个
Shell
程序,这个
Shell
远远不如
Bash
复杂,但也能满足我们一般的使用,下面,我们就开始。
首先,给这个
Shell
取一个名字,不妨就叫做
Mini Shell
。
Linux
系统的命令分为内部命令和外部命令两种,内部命令由
Shell
程序实现,如
cd
、
echo
等,
Linux
的内部命令数量有限,而且绝大部分都很少用到。而每一个
Linux
外部命令都是一个单独的应用程序,我们非常熟悉的
ls
、
cp
等绝大多数命令都是外部命令,这些命令都以可执行文件的形式存在,绝大部分放在目录
/bin
和
/sbin
中。这样一来,我们编程的难度就可以大大下降了,我们只需要实现很有限的内部命令,对于其它的输入,统统当作应用程序来执行即可。
为了简单明了起见,
Mini Shell
只实现了
2
个内部命令:
1
、
cd
用于切换目录,和我们熟悉的命令
cd
类似,除了没有那么多的附加功能。
2
、
quit
用于退出
Mini Shell
。
下面是程序清单:
1: /* mshell.c */
2: #include <sys/types.h>
1: #include <unistd.h>
3: #include <sys/wait.h>
4: #include <string.h>
5: #include <errno.h>
6: #include <stdio.h>
7:
9: void do_cd(char *argv[]);
10: void execute_new(char *argv[]);
11:
12: main()
13: {
14: char *cmd=(void *)malloc(256*sizeof(char));
15: char *cmd_arg[10];
16: int cmdlen,i,j,tag;
17:
18: do{
19: /*
初始化cmd */
20: for(i=0;i<255;i++) cmd[i]='\0';
21:
22: printf("-=Mini Shell=-*| ");
23: fgets(cmd,256,stdin);
24:
25: cmdlen=strlen(cmd);
26: cmdlen--;
27: cmd[cmdlen]='\0';
28:
29
: /* 把命令行分解为指针数组cmd_arg */
30
: for(i=0;i<10;i++) cmd_arg[i]=NULL;
31
: i=0; j=0; tag=0;
32
: while(i<cmdlen && j<10){
33
: if(cmd[i]==' '){
34
: cmd[i]='\0';
35
: tag=0;
36
: }else{
37
: if(tag==0)
38
: cmd_arg[j++]=cmd+i;
39
: tag=1;
40
: }
41
: i++;
42
: }
43
:
44
: /* 如果参数超过10个,就打印错误,并忽略当前输入 */
45
: if(j>=10 && i<cmdlen){
46
: printf("TOO MANY ARGUMENTS\n");
47
: continue;
48
: }
49
:
50
: /* 命令quit:退出Mini Shell */
51
: if(strcmp(cmd_arg[0],"quit")==0)
52
: break;
53
:
54
: /* 命令cd */
55
: if(strcmp(cmd_arg[0],"cd")==0){
56
: do_cd(cmd_arg);
57
: continue;
58
: }
59
:
60
: /* 外部命令或应用程序 */
61
: execute_new(cmd_arg);
62
: }while(1);
63
: }
64
:
65
: /* 实现cd的功能 */
66
: void do_cd(char *argv[])
67
: {
68
: if(argv[1]!=NULL){
69
: if(chdir(argv[1])<0)
70
: switch(errno){
71
: case ENOENT:
72
: printf("DIRECTORY NOT FOUND\n");
73
: break;
74
: case ENOTDIR:
75
: printf("NOT A DIRECTORY NAME\n");
76
: break;
77
: case EACCES:
78
: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n");
79
: break;
80
: default:
81
: printf("SOME ERROR HAPPENED IN CHDIR\n");
82
: }
83
: }
84
:
85
: }
86
:
87
: /* 执行外部命令或应用程序 */
88
: void execute_new(char *argv[])
89
: {
90
: pid_t pid;
91
:
92
: pid=fork();
93
: if(pid<0){
94
: printf("SOME ERROR HAPPENED IN FORK\n");
95
: exit(2);
96
: }else if(pid==0){
97
: if(execvp(argv[0],argv)<0)
98
: switch(errno){
99
: case ENOENT:
100
: printf("COMMAND OR FILENAME NOT FOUND\n");
101
: break;
102
: case EACCES:
103
: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n");
104
: break;
105
: default:
106
: printf("SOME ERROR HAPPENED IN EXEC\n");
107
: }
108
: exit(3);
109
: }else
110
: wait(NULL);
111
: }
|
这个程序稍稍有点长,我们来对它作一下详细的解释:
函数
main
:
14
行:定义字符串
cmd
,用于接收用户输入的命令行。
15
行:定义指针数组
cmd_arg
,它的形式和作用都与我们熟悉的
char *argv[]
一样。
从以上
2
个定义可以看出
Mini Shell
对命令输入的
2
个限制:首先,用户输入的命令行必须在
255
个字符之内(除去字符串结束标志
'\0'
);其次,命令行的参数个数不得超过
10
个(包括命令本身在内)。
18
行:进入一个
do-while
循环,这个循环是本程序的主体部分,基本思想是
"
等待输入命令
--
处理已输入命令
--
等待输入命令
"
。
22
行:打印输入提示信息。在
Mini Shell
中,你可以随意定自己喜欢的命令输入提示信息,本程序中使用了
"-=Mini Shell=-*| "
,是不是有点像一个
CS
高手?如果不喜欢,你可以用任意的字符替换它。
23
行:接收用户输入。
25-27
行:
fgets
接受输入时,会将输入字符串时末尾的换行符(
"\n"
)一起接受,这是我们不需要的,所以要把它去掉。本程序中简单的用字符串结束标志
'\0'
覆盖了字符串
cmd
的最后一个字符来实现这个目的。
30
行:初始化指针数组
cmd_arg
。
32-42
行:对输入进行分析,将
cmd
中参数间的空格用
'\0'
填充,并把各参数的起始地址分别赋与
cmd_arg
数组。这样就把
cmd
分解成了
cmd_arg
,但分解后的各命令参数仍然使用着
cmd
的内存空间,所以在命令执行结束前不宜对
cmd
另外赋值。
45
行:如果还未分析到输入字符串的末尾(
i<cmdlen
),而分析出的参数已经达到或超过了
10
个(
j>=10
),就认为输入的命令行超出了
10
个参数的限制,打印错误并重新接收命令。
51-52
行:内部命令
quit
:字符串
cmd_arg[0]
就是命令本身,如果命令是
quit
,则退出循环,也就等于退出该程序。
55-58
行:内部命令
cd
:调用函数
do_cd()
完成
cd
命令的动作。
61
行:对于其它的外部命令和应用程序,调用函数
execute_new()
执行。
函数
do_cd
:
68
行:仅仅考虑紧跟在命令后面的参数
argv[1]
,而不再考虑其它的参数。如果这个参数存在,就把它作为要转换的目录。
69
行:调用系统调用
chdir
切换当前目录,参见附录
1
。
70-82
行:对
chdir
可能出现的错误进行处理。
函数
execute_new
:
92
行:调用系统调用
fork
产生新的子进程。
93
行:如果返回负值,说明
fork
调用出现错误。
96
行:如果返回
0
,说明当前进程是子进程。
97
行:调用
execvp
执行新的应用程序,并检测调用是否出错(返回负值)。这里使用
execvp
的原因是它可以自动在各默认目录里寻找目标应用程序的位置,而不必我们自己编程实现。
98-107
行:对
execvp
可能出现的错误进程处理。
108
行:如果
execvp
的执行出现错误,子进程在这里终止。表面上看起来,这个
exit
是接着
97
行的错误判断的下一行语句,而非
if
语句的一部分,似乎无论调用
execvp
成功与否都会接着执行
exit
。但事实上,如果
execvp
调用成功的话,这个进程将会被新的程序代码填充,因而根本不可能执行到这一行。反之,如果执行到了这一行,说明前面的
execvp
调用一定出现了错误。这样的效果和
exit
被包含在
if
语句中的效果是完全一样的。
109
行:如果
fork
返回其它值,说明当前进程是父进程。
110
行:调用系统调用
wait
。
wait
在这里有两个作用:
1.
使父进程在此暂停,等待子进程执行完毕。这样,就可以等子进程的所有信息全部输出完毕后才打印命令提示符,等待下一条命令的输入,从而避免了命令提示符和应用程序输出混杂在一起的现象。
2.
收集子进程退出后留下的僵尸进程。可能有读者一直对这个问题存有疑问
--"
我们编程生成的子进程由我们自己设计的父进程负责收集,但我们手动执行的这个父进程由谁收集呢?
"
现在大家应该明白了,我们从命令行执行的所有进程最后都是由
shell
收集的。
关于
Mini Shell
的编译和运行,这里就不再敷述了,有兴趣的读者可以自行动手实验,或者对这个程序进行改进,使之更接近甚至超过我们正使用的
Bash
。
1.14 daemon
进程
1.14.1
了解
daemon
进程
这又是一个有趣的概念,
daemon
在英语中是
"
精灵
"
的意思,就像我们经常在迪斯尼动画里见到的那些,有些会飞,有些不会,经常围着动画片的主人公转来转去,啰里啰唆地提一些忠告,时不时倒霉地撞在柱子上,有时候还会想出一些小小的花招,把主人公从敌人手中救出来,正因如此,
daemon
有时也被译作
"
守护神
"
。所以,
daemon
进程在国内也有两种译法,有些人译作
"
精灵进程
"
,有些人译作
"
守护进程
"
,这两种称呼的出现频率都很高。
与真正的
daemon
相似,
daemon
进程也习惯于把自己隐藏在人们的视线之外,默默为系统做出贡献,有时人们也把它们称作
"
后台服务进程
"
。
daemon
进程的寿命很长,一般来说,从它们一被执行开始,直到整个系统关闭,它们才会退出。几乎所有的服务器程序,包括我们熟知的
Apache
和
wu -FTP
,都用
daemon
进程的形式实现。很多
Linux
下常见的命令如
inetd
和
ftpd
,末尾的字母
d
就是指
daemon
。
为什么一定要使用
daemon
进程呢?
Linux
中每一个系统与用户进行交流的界面称为终端(
terminal
),每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端(
Controlling terminal
),当控制终端被关闭时,相应的进程都会被自动关闭。关于这点,读者可以用
X-Window
中的
XTerm
试验一下,(每一个
XTerm
就是一个打开的终端,)我们可以通过键入命令启动应用程序,比如:
$netscape
然后我们关闭
XTerm
窗口,刚刚启动的
netscape
窗口也会随之一同突然蒸发。但是
daemon
进程却能够突破这种限制,即使对应的终端关闭,它也能在系统中长久地存在下去,如果我们想让某个进程长命百岁,不因为用户或终端或其他的变化而受到影响,就必须把这个进程变成一个
daemon
进程。
1.14.2
daemon
进程的编程规则
如果想把自己的进程变成
daemon
进程,我们必须严格按照以下步骤进行:
1.
调用
fork
产生一个子进程,同时父进程退出。我们所有后续工作都在子进程中完成。这样做我们可以:
1.
如果我们是从命令行执行的该程序,这可以造成程序执行完毕的假象,
shell
会回去等待下一条命令;
2.
刚刚通过
fork
产生的新进程一定不会是一个进程组的组长,这为第
2
步的执行提供了前提保障。
这样做还会出现一种很有趣的现象:由于父进程已经先于子进程退出,会造成子进程没有父进程,变成一个孤儿进程(
orphan
)。每当系统发现一个孤儿进程,就会自动由
1
号进程收养它,这样,原先的子进程就会变成
1
号进程的子进程。
2.
调用
setsid
系统调用。这是整个过程中最重要的一步。
setsid
的介绍见附录
2
,它的作用是创建一个新的会话(
session
),并自任该会话的组长(
session leader
)。如果调用进程是一个进程组的组长,调用就会失败,但这已经在第
1
步得到了保证。调用
setsid
有
3
个作用:
1.
让进程摆脱原会话的控制;
2.
让进程摆脱原进程组的控制;
3.
让进程摆脱原控制终端的控制;
总之,就是让调用进程完全独立出来,脱离所有其他进程的控制。
3.
把当前工作目录切换到根目录。如果我们是在一个临时加载的文件系统上执行这个进程的,比如:
/mnt/floppy/
,该进程的当前工作目录就会是
/mnt/floppy/
。在整个进程运行期间该文件系统都无法被卸下(
umount
),而无论我们是否在使用这个文件系统,这会给我们带来很多不便。解决的方法是使用
chdir
系统调用把当前工作目录变为根目录,应该不会有人想把根目录卸下吧。关于
chdir
的用法,参见附录
1
。
当然,在这一步里,如果有特殊的需要,我们也可以把当前工作目录换成其他的路径,比如
/tmp
。
4.
将文件权限掩码设为
0
。这需要调用系统调用
umask
,参见附录
3
。每个进程都会从父进程那里继承一个文件权限掩码,当创建新文件时,这个掩码被用于设定文件的默认访问权限,屏蔽掉某些权限,如一般用户的写权限。当另一个进程用
exec
调用我们编写的
daemon
程序时,由于我们不知道那个进程的文件权限掩码是什么,这样在我们创建新文件时,就会带来一些麻烦。所以,我们应该重新设置文件权限掩码,我们可以设成任何我们想要的值,但一般情况下,大家都把它设为
0
,这样,它就不会屏蔽用户的任何操作。
如果你的应用程序根本就不涉及创建新文件或是文件访问权限的设定,你也完全可以把文件权限掩码一脚踢开,跳过这一步。
5.
关闭所有不需要的文件。同文件权限掩码一样,我们的新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不被我们的
daemon
进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。需要指出的是,文件描述符为
0
、
1
和
2
的三个文件(文件描述符的概念将在下一章介绍),也就是我们常说的输入、输出和报错这三个文件也需要被关闭。很可能不少读者会对此感到奇怪,难道我们不需要输入输出吗?但事实是,在上面的第
2
步后,我们的
daemon
进程已经与所属的控制终端失去了联系,我们从终端输入的字符不可能达到
daemon
进程,
daemon
进程用常规的方法(如
printf
)输出的字符也不可能在我们的终端上显示出来。所以这三个文件已经失去了存在的价值,也应该被关闭。
下面,就然我们亲眼看一个
daemon
进程的诞生:
1.14.3
一个
daemon
程序
/* daemon.c */
#include<unistd.h>
#include<sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535
main()
{
pid_t pid;
int i;
pid=fork();
if(pid<0){
printf("error in fork\n");
exit(1);
}else if(pid>0)
/*
父进程退出 */
exit(0);
/*
调用setsid */
setsid();
/*
切换当前目录 */
chdir("/");
/*
设置文件权限掩码 */
umask(0);
/*
关闭所有可能打开的不需要的文件 */
for(i=0;i<MAXFILE;i++)
close(i);
/*
到现在为止,进程已经成为一个完全的daemon进程,
你可以在这里添加任何你要daemon做的事情,如:
*/
for(;;)
sleep(10);
}
|
编译和运行的任务就交给读者们自己完成。
daemon
进程不像其他进程一样有很抢眼的运行结果,基本上它只是毫不声张地做自己的事。你不可能看到任何东西,但可以用
"ps -ajx"
命令观察一下你的
daemon
进程的状态和一些参数。
1.15
附录
1.15.1
系统调用
chdir
#include <unistd.h>
int chdir(const char *path);
|
chdir
的作用是改变当前工作目录。进程的当前工作目录一般是应用程序启动时的目录,一旦进程开始运行后,当前工作目录就会保持不变,除非调用
chdir
。
chdir
只有
1
个字符串参数,就是要转去的路径。例如:
chdir("/");
|
进程的当前路径就会变为根目录。
1.15.2
系统调用
setsid
#include <unistd.h>
pid_t setsid(void);
|
一个会话(
session
)开始于用户登陆,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话,除非进程调用
setsid
系统调用。
系统调用
setsid
不带任何参数,调用之后,调用进程就会成立一个新的会话,并自任该会话的组长。
1.15.3
系统调用
umask
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
|
系统调用
umask
可以设定一个文件权限掩码,用户可以用它来屏蔽某些权限,以防止误操作导致给予某些用户过高的权限。