关于linux进程编程的一些笔记整理

关于linux进程编程的一些笔记整理。

涉及到通过系统调用对进程的创建控制以及进程间通讯的实现。
 
Linux 系统调用 --fork 函数详解
  功能描述:
建立一个子进程。所建立的子进程 PID PPID 不同于其父进程,同时资源使用被设置为 0 ,文件锁和挂起的信号不被继承。
Linux 内部, fork 的执行使用 copy-on-write 页面,所以耗费的资源只是拷贝父进程页表,建立唯一子进程 task 结构体的时间和内存。
用法:  
#include
#include
 
pid_t fork(void);
 
返回说明:   
成功执行时,在父进程的线程执行中返回子进程 PID ,在子进程的线程执行中返回 0 。失败时在父进程的语境中返回 -1 errno 被设为以下的某个值   
EAGAIN
1. 不能为拷贝父进程的页表和子进程的 task 结构分配足够的内存
2. 不可能再建立新的子进程,因为调用者已达到 RLIMIT_NPROC 的资源限制,除了调用进程有 CAP_SYS_ADMIN CAP_SYS_RESOURCE 的权限
ENOMEM :内核内存不足,无法分配必须的数据结构
 
例子:
 
#include
#include
#include
 
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 /* 提供类型 pid_t 的定义 */
         #include /* 提供函数的定义 */
pid_t getpid(void);
 
getpid 的作用很简单,就是返回当前进程的进程 ID ,请大家看以下的例子:
/* getpid_test.c *
/#include
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   /* 提供类型 pid_t 的定义 */
         #include /* 提供函数的定义 */
         pid_t fork(void);
 
只看 fork 的名字,可能难得有几个人可以猜到它是做什么用的。 fork 系统调用的作用是复制一个进程。当一个进程调用它,完成后就出现两个几乎一模一样的进程,我们也由此得到了一个新进程。据说 fork 的名字就是来源于这个与叉子的形状颇有几分相似的工作流程。
Linux 中,创造新进程的方法只有一个,就是我们正在介绍的 fork 。其他一些库函数,如 system() ,看起来似乎它们也能创建新的进程,如果能看一下它们的源码就会明白,它们实际上也在内部调用了 fork 。包括我们在命令行下运行应用程序,新的进程也是由 shell 调用 fork 制造出来的。 fork 有一些很有意思的特征,下面就让我们通过一个小程序来对它有更多的了解。
 
/* fork_test.c */
#include
#inlcude
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
         void exit(int status);
 
不像 fork 那么难理解,从 exit 的名字就能看出,这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到 exit 系统调用,进程就会停止剩下的所有操作,清除包括 PCB 在内的各种数据结构,并终止本进程的运行。请看下面的程序:
 
/* exit_test1.c */
#include
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
         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
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
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 #include 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 ]  1579 pts/0    R      0:00 ps -ax

看到中间的 "Z" 了吗?那就是僵尸进程的标志,它表示 1578 号进程现在就是一个僵尸进程。
我们已经学习了系统调用 exit ,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。僵尸进程虽然对其他进程几乎没有什么影响,不占用 CPU 时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服。而且 Linux 系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。那么,我们该如何来消灭这些僵尸进程呢?
先来了解一下僵尸进程的来由,我们知道, Linux UNIX 总有着剪不断理还乱的亲缘关系,僵尸进程的概念也是从 UNIX 上继承来的,而 UNIX 的先驱们设计这个东西并非是因为闲来无聊想烦烦其他的程序员。僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统 CPU 时间和总用户 CPU 时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就只好干瞪眼了。
那么,我们如何收集这些信息,并终结这些僵尸进程呢?就要靠我们下面要讲到的 waitpid 调用和 wait 调用。这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。下面就对这两个调用分别作详细介绍。
1.8 wait
1.8.1 简介
wait 的函数原型是:

                 #include /* 提供类型pid_t的定义 */          #include          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 #include #include #include 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 #include #include 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 #include 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 waitpid
1.9.1 简介
waitpid 系统调用在 Linux 函数库中的原型是:

                 #include /* 提供类型pid_t的定义 */          #include          pid_t waitpid(pid_t pid,int *status,int options)         

 
从本质上讲,系统调用 waitpid wait 的作用是完全相同的,但 waitpid 多出了两个可由用户控制的参数 pid options ,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
pid
从参数的名字 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
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 #include #include 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 秒钟的工作。父子进程都有工作要做,父进程利用工作的简短间歇察看子进程的是否退出,如退出就收集它。
 

 
1.10 exec
也许有不少读者从本系列文章一推出就开始读,一直到这里还有一个很大的疑惑:既然所有新进程都是由 fork 产生的,而且由 fork 产生的子进程和父进程几乎完全一样,那岂不是意味着系统中所有的进程都应该一模一样了吗?而且,就我们的常识来说,当我们执行一个程序的时候,新产生的进程的内容应就是程序的内容才对。是我们理解错了吗?显然不是,要解决这些疑惑,就必须提到我们下面要介绍的 exec 系统调用。
1.10.1 简介
说是 exec 系统调用,实际上在 Linux 中,并不存在一个 exec() 的函数形式, exec 指的是一组函数,一共有 6 个,分别是:

#include 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 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 1:       #include 3:       #include 4:       #include 5:       #include 6:       #include 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 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 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 ),而分析出的参数已经达到或超过了 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 #include #include #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                  close(i);          /*             到现在为止,进程已经成为一个完全的daemon进程,             你可以在这里添加任何你要daemon做的事情,如:          */          for(;;)                  sleep(10); }

 
编译和运行的任务就交给读者们自己完成。 daemon 进程不像其他进程一样有很抢眼的运行结果,基本上它只是毫不声张地做自己的事。你不可能看到任何东西,但可以用 "ps -ajx" 命令观察一下你的 daemon 进程的状态和一些参数。
1.15 附录
1.15.1 系统调用 chdir

       #include        int chdir(const char *path);       

 
chdir 的作用是改变当前工作目录。进程的当前工作目录一般是应用程序启动时的目录,一旦进程开始运行后,当前工作目录就会保持不变,除非调用 chdir chdir 只有 1 个字符串参数,就是要转去的路径。例如:

chdir("/");

 
进程的当前路径就会变为根目录。
1.15.2 系统调用 setsid

       #include        pid_t setsid(void);       

 
一个会话( session )开始于用户登陆,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话,除非进程调用 setsid 系统调用。
系统调用 setsid 不带任何参数,调用之后,调用进程就会成立一个新的会话,并自任该会话的组长。
1.15.3 系统调用 umask

       #include        #include        mode_t umask(mode_t mask);       

 
系统调用 umask 可以设定一个文件权限掩码,用户可以用它来屏蔽某些权限,以防止误操作导致给予某些用户过高的权限。

你可能感兴趣的:(LINUX系统开发,linux,编程,cmd,shell,null,path)