六狼论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

新浪微博账号登陆

只需一步,快速开始

搜索
查看: 48|回复: 0

Linux编程每周一讲-进程

[复制链接]

升级  31.33%

90

主题

90

主题

90

主题

举人

Rank: 3Rank: 3

积分
294
 楼主| 发表于 2013-2-4 13:23:09 | 显示全部楼层 |阅读模式
进程可能是Linux世界下最有生命力的存在了。所谓进程就是活的程序,正在运行中的代码,它的英文名叫Process。当然后来还有了更轻量级的线程(Thread),但是它是存活在进程的体内的,不能独立于进程存在。所以,本文要讲的就是:Process,Linux下的进程。

话说天地初开之际,计算机还是一堆插着电的废铁,必须要有软件,人类才可能使用它。所以,程序猿和硬件工程师们一起创造了BIOS,也就是固化在硬件当中的软件(我们管它叫固件,Firmware)。当计算机通电后,由BIOS负责将计算机里面的各个硬件加载,运转起来,伟大的科学家们给这一过程非常形象地起了一个名字:Bootstrap。

这个词初见于 埃·拉斯伯和戈·毕尔格 这两位德国人的童话故事 《吹牛大王历险记》:在书中,男主角敏豪森男爵骑着马掉进了沼泽,但他天生神力,两腿夹着马,双手抓着自己的小辫子,把自已和马一起从沼泽里拉到了半空!

因此,你懂的,计算机的启动过程就比较类似于这样。至于为什么后来这个启动过程变成了叫做Bootstrap,即拉着自己的鞋跟,而不是拉着自己的小辫子,这就说不清了。历史上一个说法是,这个德国民间故事后来传到美国,就被再加工,变成了拉鞋跟了。。。

好吧,我们是在说计算机。当BIOS完成Bootstrap后,会把控制权交接给操作系统。此时Linux系统会执行Bootloader,比如GRUB或者LILO,让你选择要运行的Linux系统;对于没有安装Bootloader的Linux系统,则跳过选择这一步,总之接下来是开始Linux系统的加载过程。

此时,一个编号为0的进程产生了,它是所有后续进程的父。这个编号为0的进程会产生一个进程为1的进程,然后自我毁灭(准确来讲,是变成swapper进程)。这个进程为1的进程就是init,它是继而成为所有后续进程的父。

Linux下面的进程,就是这样父与子的层级关系,一个父进程可以有多个孩子(废话,否则init以下只有一条线,Linux就成单任务操作系统了);一个孩子只有一个父(嗯,两个父亲比较麻烦)。

以上是两个神级的进程,我们可以用ps命令看一下init:

% ps -ef | grep initUID        PID  PPID  C STIME TTY          TIME CMDroot         1     0  0 Dec04 ?        00:00:01 /sbin/init

可以看到,init的进程号是1(PID),而它的父亲ID是0(PPID)。

接下来,我们来做一个多进程的程序,这个程序包含两个进程,一个是父亲,一个是孩子。

在Linux的世界下,我们使用fork()函数来创建一个新的进程。当程序调用fork()时,会针对当前代码产生一个新的进程。这个函数比较有意思,它会返回两次:对父亲进程返回孩子进程的PID(进程号),对孩子进程则返回0。

这样,我们在代码中就可以通过判断fork的返回,来确定自己是孩子还是父亲了。废话不多说,上代码:

*process.c*

/* exit() */#include <stdlib.h>#include <sys/types.h>/* * fork() */#include <unistd.h>/*  * stderr * stdout * fprintf */#include <stdio.h>void err_sys(const char* fmt, ...);int main () {pid_t pid;if ((pid = fork()) < 0) { /* 创建新的进程 */err_sys("fork error");} else if (pid == 0) { /* fork向孩子进程返回0 *//* 所以只有孩子进程会执行到这里 */puts("child process here."); puts("child process will sleep for 10 seconds...");sleep(10);} else { /* fork向父进程返回孩子进程的PID *//* 所以只有父进程会执行到这里 */puts("parent process here.");printf("parent get the child pid: %d\n", pid);}/* 父亲孩子共用的代码部分 *//* 注意,虽然代码是共用的,但内存资源是独立的 *//* 父亲是访问不到孩子的变量,孩子也访问不到父亲的变量 *//* 所以说进程是很重的,资源会被复制。*/sleep(3);if (pid == 0) { /* child process */puts("child process exit.");} else { /* parent process */puts("parent process exit.");}/* share part of child and parent */return 0;}void err_sys(const char* fmt, ...) {/* man stdarg to see the instructions on va_list */va_list ap;fprintf(stderr, fmt, ap);exit(1);}

编译上面的代码:

cc process.c -o process.o

然后运行代码:

% ./process.oparent process here.parent get the child pid: 1650child process here.child process will sleep for 10 seconds...

此时查看进程:

% ps -ef | grep process.oUID        PID  PPID  C STIME TTY          TIME CMDweli      1649  1407  0 00:54 pts/1    00:00:00 ./process.oweli      1650  1649  0 00:54 pts/1    00:00:00 ./process.o

可以看到,此时一父一子两进程都在运行。并且从PID及PPID可以看明白父子关系。

我们的代码会让两个进程都睡眠三秒钟,而子进程则会多睡眠10秒,所以肯定是父进程先退出。所以3秒钟后我们会看到终端输出:

parent process exit.

此时查看进程情况:

% ps -ef | grep process.oUID        PID  PPID  C STIME TTY          TIME CMDweli      1650     1  0 00:54 pts/1    00:00:00 ./process.o

发现只有子进程在运行了,有趣的是PPID,可以看到此时这个进程的父级进是1号进程,即init了。这就是Linux的系统设计:当父进程退出时,子进程init来接管。

再等一会,子进程也退出了:

child process exit.

僵尸进程(Zombie)

在上一节最后,我们知道,如果父进程先退出了,子进程会被init接管成为其子进程。那么如果子进程先退出,父进程还在执行,会发生什么呢?

这种情况下,子进程执行完成后,释放掉自己占用的资源,仅留一个壳(进程号和一些其它信息),等待父进程执行完成后,一起由内核消毁掉。这种只剩一个壳的进程,我们称之为僵尸(Zombie)进程。我们可以通过让子进程先结束,父进程后结束的方式来制造一个Zombie:

*zombie.c*

/* exit() */#include <stdlib.h>#include <sys/types.h>/* * fork() */#include <unistd.h>/*  * stderr * stdout * fprintf */#include <stdio.h>void err_sys(const char* fmt, ...);int main () {pid_t pid;if ((pid = fork()) < 0) { /* create a process */err_sys("fork error");} else if (pid == 0) { /* child process *//* Child process exit immediately. * It will create a zombie process,  * and waiting for parent to handle it. */exit (0); } else {/* Parent process sleep, so we can see *//* child process in zombie state. */fputs("parent process goes to sleep...", stderr);sleep (60); }return 0;}void err_sys(const char* fmt, ...) {/* man stdarg to see the instructions on va_list */va_list ap;fprintf(stderr, fmt, ap);exit(1);}

如上面的代码所示,子进程创建后立即退出,而父进程要等待60秒后退出。编译这个代码:

cc zombie.c -o zombie.o

然后运行编译好的代码:

% ./zombie.o parent process goes to sleep...

此时查看进程运行情况:

% ps -ef | grep zombie.oUID        PID  PPID  C STIME TTY          TIME CMDweli      1683  1407  0 01:10 pts/1    00:00:00 ./zombie.oweli      1684  1683  0 01:10 pts/1    00:00:00 [zombie.o] <defunct>

可以看到,上面的子进程(PID为1684,它的父进程PPID是1683)处于defunct状态,如果我们用lsof来查看它使用的资源:

lsof -p 1684

会发现什么返回也没有,实际上这个进程就剩一个壳,没有任何资源的使用了。与正在运行的父进程相比较:

% lsof -p 1683COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF   NODE NAMEzombie.o 1683 weli  cwd    DIR  253,2     4096 131119 /home/weli/projs/learnczombie.o 1683 weli  rtd    DIR  253,1     4096      2 /zombie.o 1683 weli  txt    REG  253,2     5404 131329 /home/weli/projs/learnc/zombie.ozombie.o 1683 weli  mem    REG  253,1   157200  28423 /lib/ld-2.14.90.sozombie.o 1683 weli  mem    REG  253,1  1991260  28429 /lib/libc-2.14.90.sozombie.o 1683 weli    0u   CHR  136,1      0t0      4 /dev/pts/1zombie.o 1683 weli    1u   CHR  136,1      0t0      4 /dev/pts/1zombie.o 1683 weli    2u   CHR  136,1      0t0      4 /dev/pts/1

将<defunct>状态的进程称为僵尸进程确实很贴切,它已经没有了生命只剩外壳。

问题来了,为什么Linux要这样设计呢?子进程直接销毁掉不是更好吗?这样设计的原因是:可能父进程需要了解子进程的执行结束情况,所以它会把处理已运行完的子进程的权力交给父进程,如果父进程直到退出也没有处理这个已经运行结束的子进程,再由init来收回。

因此,父进程可通过几个函数来根据子进程的运行情况进行处理,方便之一是使用wait()函数:

pid_t wait(int *stat_loc);

在父亲进里调用wait()函数,则会使父进程等待所有子进程结束后,再继续向下运行。如果只想等待某个特定的子进程,则可使用waitpid()函数。

pid_t waitpid(pid_t pid, int *stat_loc, int options);

需要注意的是,wait和waitpid函数会block住父进程,直到子进程结束后,才能允许父进程继续运行后续代码。如果父进程在执行的任务需要比较长的时间,等待子进程的这段时间显然是浪费的。

还好Linux系统设计考虑到了这种情况,当子进程结束时,会给父进程发一个SIGCHLD信号,我们可以利用这个信号,让父进程只有接收到信号时,才去触发wait收回子进程(或使用waitpid收回结束的那个子进程)。

下面是代码:

*zombie_fix.c*

/* exit() */#include <stdlib.h>#include <sys/types.h>/* * fork() */#include <unistd.h>/*  * stderr * stdout * fprintf */#include <stdio.h>/* We'll catch SIGCHLD signal to know that child process has quit */#include <signal.h>void err_sys(const char* fmt, ...);static void child_quit(int);int main () {pid_t pid;int i;if ((pid = fork()) < 0) { /* create a process */err_sys("fork error");} else if (pid == 0) { /* child process *//* Child process will quit after 3 seconds. * It will create a zombie process,  * and waiting for parent to handle it. */puts("child process start.");sleep(3);exit (0); } else {if (signal(SIGCHLD, child_quit) == SIG_ERR)err_sys("cannot register signal handler for SIGCHLD!");puts("signal handler registered for SIGCHLD.");/* Parent process will do some work now */for (i = 0; i < 10; i++) {sleep (1); printf("#%d: parent is doing work.\n", i);}}return 0;}static void child_quit(int signo) {wait(NULL);puts("child process quit.");}void err_sys(const char* fmt, ...) {/* man stdarg to see the instructions on va_list */va_list ap;fprintf(stderr, fmt, ap);exit(1);}

可以看到,我们将wait()方法放在了信号处理函数(Signal Handler):child_quit()当中,并在父进程中将这个signal handler注册给了SIGCHLD这个信号。当子进程退出时,会给父进程发SIGCHLD信号,并触发child_quit()执行。这样,父进程就不必被wait()给block住,并可以执行下面的工作。

编译这个代码:

cc zombie_fix.c -o zombie_fix.o

然后运行:

% ./zombie_fix.osignal handler registered for SIGCHLD.child process start.#0: parent is doing work.#1: parent is doing work.child process quit.#2: parent is doing work.#3: parent is doing work....

可以看到,父进程并未等待子进程的运行,并且当子进程退出后(child process quit),查看进程运行情况:

% ps -ef | grep zombie_fix.oweli      1750  1407  0 01:52 pts/1    00:00:00 ./zombie_fix.o

可以看到只剩父进程在运行,并没有defunct状态的僵尸子进程,说明child_quit()被正确触发并执行了wait(),收回了已结束的子进程。

轻量级进程 vfork

使用vfork()函数可以创建轻量级的进程,和fork不同,使用vfork创建的子进程,会和父进程共用一套内存空间,这一点有点像线程Thread,但它又比线程重一点点,因为线程运行于一个进程之中。而vfork还是会创建一个新的进程。

需要注意的是,由于共用内存空间,在子进程改变一个变量的值,父进程里面的变量值也会跟着变,因为它们本来就是共用一处内存。下面是代码示例:

*vfork.c*

/*  * This code is referenced from APUE 1st edition, program 8.2, with slight modifications. *//*  * fprintf()  */#include <stdio.h>/* * vfork() * _exit() * getpid() */#include <unistd.h>/* * exit() */#include <stdlib.h>int glob = 1; /* external variable in initialized data */void err_sys(const char* fmt, ...);int main() {int var; /* automatic variable on the stack */pid_t pid;var = 1;printf("before vfork\n");if ((pid = vfork()) < 0) err_sys("vfork error");else if (pid == 0) { /* child *//*  * Because vfork makes child to share address space with parent, * the child process will increment the glob variable in parent space; */printf("modifying variables in parent address space.\n");glob++; var++;/*  * Compared with exit(), _exit() won't flush io and close the stdout. Because we * are sharing address space with parent, we must use _exit() so in following * parent printf() call, the parent could output to stdout. If we use exit here  * instead, the printf() in parent won't print anything. */_exit(0); }/* parent *//*  * if we use exit() instead of _exit() in child process, printf won't output anything, * because stdout is already closed. */printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var); exit(0);}void err_sys(const char* fmt, ...) {va_list ap;fprintf(stderr, fmt, ap);exit(1);}

运行结果大家可以先猜猜看,然后编译上面代码看看自己分析的是否正确。

Daemon Process

试着从网上找资料,自己了解一下什么是Daemon Process以及它的用法。

小结

我将上面用到的代码放在了github上面:

http://github.com/liweinan/learnc

可以签出后玩玩看。
您需要登录后才可以回帖 登录 | 立即注册 新浪微博账号登陆

本版积分规则

快速回复 返回顶部 返回列表