关于系统进程

本文深入探讨Linux的进程管理,介绍进程调度、进程ID、fork、exit及_exit系统调用,并通过实例展示如何创建、管理及终止进程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

Linux 是一个多任务的操作系统,也就是说,在同一个时间内,可以有多个进程同时执行。如果读者对计算机硬件体系有一定了解的话,会知道我们大家常用的单CPU 计算机实际上在一个时间片断内只能执行一条指令,那么Linux 是如何实现多进程同时执行的呢?原来Linux 使用了一种称为" 进程调度(process scheduling" 的手段,首先,为每个进程指派一定的运行时间,这个时间通常很短,短到以毫秒为单位,然后依照某种规则,从众多进程中挑选一个投入运行,其他的进程暂时等待,当正在运行的那个进程时间耗尽,或执行完毕退出,或因某种原因暂停,Linux 就会重新进行调度,挑选下一个进程投入运行。因为每个进程占用的时间片都很短,在我们使用者的角度来看,就好像多个进程同时运行一样了。

Linux 中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(Process Control Block ,简称PCB )。PCB 中包含了很多重要的信息,供系统调度和进程本身执行使用,其中最重要的莫过于进程IDprocess 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]

以上除标题外,每一行都代表一个进程。在各列中,PID 一列代表了各进程的进程IDCOMMAND 一列代表了进程的名称或在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

",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

",getpid());

else

printf("I am the parent process, my process ID is %d

",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!

");

exit(0);

printf("never be displayed!

");

}

编译后运行:

$gcc exit_test1.c -o exit_test1

$./exit_test1

this process will exit!

我们可以看到,程序并没有打印后面的"never be displayed! " ,因为在此之前,在执行到exit(0) 时,进程就已经终止了。

exit 系统调用带有一个整数类型的参数status ,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0 表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。我们在实际编程时,可以用wait 系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。关于wait 的详细情况,我们将在以后的篇幅中进行介绍。

exit_exit

作为系统调用而言,_exitexit 是一对孪生兄弟,它们究竟相似到什么程度,我们可以从Linux 的源码中找到答案:

#define __NR__exit __NR_exit /* 摘自文件include/asm-i386/unistd.h334*/

"__NR_" 是在Linux 的源码中为每个系统调用加上的前缀,请注意第一个exit 前有2 条下划线,第二个exit 前只有1 条下划线。

这时随便一个懂得C 语言并且头脑清醒的人都会说,_exitexit 没有任何区别,但我们还要讲一下这两者之间的区别,这种区别主要体现在它们在函数库中的定义。_exitLinux 函数库中的原型是:

#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/Obuffered I/O" ,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符和文件结束符EOF ),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit() 函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit() 函数。

请看以下例程:

/* exit2.c */

#include<stdlib.h>

main()

{

printf("output begin

");

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

");

printf("content in buffer");

_exit(0);

}

编译并运行:

$gcc _exit1.c -o _exit1

$./_exit1

output begin

Linux 中,标准输入和标准输出都是作为文件处理的,虽然是一类特殊的文件,但从程序员的角度来看,它们和硬盘上存储数据的普通文件并没有任何区别。与所有其他文件一样,它们在打开后也有自己的缓冲区。

在这篇文章中,我们对Linux 的进程管理作了初步的了解,并在此基础上学习了getpidforkexit_exit 四个系统调用。在下一篇文章中,我们将学习与Linux 进程管理相关的其他系统调用,并将作一些更深入的探讨。

前面的文章中,我们已经了解了父进程和子进程的概念,并已经掌握了系统调用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 系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。那么,我们该如何来消灭这些僵尸进程呢?

先来了解一下僵尸进程的来由,我们知道,LinuxUNIX 总有着剪不断理还乱的亲缘关系,僵尸进程的概念也是从UNIX 上继承来的,而UNIX 的先驱们设计这个东西并非是因为闲来无聊想烦烦其他的程序员。僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统CPU 时间和总用户CPU 时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就只好干瞪眼了。

那么,我们如何收集这些信息,并终结这些僵尸进程呢?就要靠我们下面要讲到的waitpid 调用和wait 调用。这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。下面就对这两个调用分别作详细介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值