先看一下进程在课本里的标准定义:“进程是可并发执行的程序在一个数据集合上的运行过程。”这个定义非常严谨,而且难懂,如果你没有一下子理解这句话,就不妨看看并不严谨的解释。我们大家都知道,硬盘上的一个可执行文件经常被称作程序,在Linux系统中,当一个程序开始执行后,在开始执行到执行完毕退出这段时间里,它在内存中的部分就被称作一个进程。

当然,这个解释并不完善,但好处是容易理解,在以下的文章中,我们将会对进程作一些更全面的认识。

1.1 Linux进程简介

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

在Linux中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(Process Control Block,简称PCB)。PCB中包含了很多重要的信息,供系统调度和进程本身执行使用,其中最重要的莫过于进程ID(process ID)了,进程ID也被称作进程标识符,是一个非负的整数,在Linux操作系统中唯一地标志一个进程,其实从进程ID的名字就可以看出,它就是进程的身份证号码,每个人的身份证号码都不会相同,每个进程的进程ID也不会相同。

1.2 getpid

#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());
}

编译并运行程序getpid_test.c:

$gcc getpid_test.c -o getpid_test
$./getpid_test
The current process ID is 1980

1.3 fork

#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());elseprintf("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调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

1.在父进程中,fork返回新创建子进程的进程ID;

2.在子进程中,fork返回0;

3.如果出现错误,fork返回一个负值;

fork出错可能有两种原因:(1)当前的进程数已经达到了系统规定的上限;(2)系统内存不足。

fork系统调用出错的可能性很小,而且如果出错,一般都为第一种错误。如果出现第二种错误,说明系统已经没有可分配的内存,正处于崩溃的边缘,这种情况对Linux来说是很罕见的。

剩下的代码,如果pid小于0,说明出现了错误;pid==0,就说明fork返回了0,也就说明当前进程是子进程,就去执行printf("I am the child!"),否则(else),当前进程就是父进程,执行printf("I am the parent!")。

说到这里,可能有些读者还有疑问:如果fork后子进程和父进程几乎完全一样,而系统中产生新进程唯一的方法就是fork,那岂不是系统中所有的进程都要一模一样吗?那我们要执行新的应用程序时候怎么办呢?从对Linux系统的经验中,我们知道这种问题并不存在。至于采用了什么方法,我们到后面具体讨论。

1.4 exit

#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_test1
this process will exit!

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

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

在一个进程调用了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 -bash1577 pts/0    S      0:00 ./zombie1578 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时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就只好干瞪眼了。

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

1.5 wait

1.5.1 简介

#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。

1.5.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.5.3 进程同步

有时候,父进程要求子进程的运算结果进行下一步的运算,或者子进程的功能是为父进程提供了下一步执行的先决条件(如:子进程建立文件,而父进程写入数据),此时父进程就必须在某一个位置停下来,等待子进程运行结束,而如果父进程不等待而直接执行下去的话,可以想见,会出现极大的混乱。这种情况称为进程之间的同步,更准确地说,这是进程同步的一种特例。进程同步就是要协调好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.6 exec

也许有同学从fork开始读,一直到这里还有一个很大的疑惑:既然所有新进程都是由fork产生的,而且由fork产生的子进程和父进程几乎完全一样,那岂不是意味着系统中所有的进程都应该一模一样了吗?而且,就我们的常识来说,当我们执行一个程序的时候,新产生的进程的内容应就是程序的内容才对。是我们理解错了吗?显然不是,要解决这些疑惑,就必须提到我们下面要介绍的exec系统调用。

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

现在我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。

事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种"写时拷贝(copy-on-write)"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。

int execve(const char *path, char *const argv[ ], char *const envp[ ]);

execve第1个参数path是被执行应用程序的完整路径,第2个参数argv就是传给被执行应用程序的命令行参数,第3个参数envp是传给被执行应用程序的环境变量。

因为课程的学时有限,我们对于exec系统调用的使用就不再深入了,又兴趣的同学可查看《UNIX进程间通信》等一些关于linux编程的书。

源程序

。。。。

参考文章

浙江理工大学——操作系统实验指导 (2020.10)

Linux——进程管理相关推荐

  1. Linux进程管理 (7)实时调度

    关键词:RT.preempt_count.RT patch. 除了CFS调度器之外,还包括重要的实时调度器,有两种RR和FIFO调度策略.本章只是一个简单的介绍. 更详细的介绍参考<Linux进 ...

  2. linux进程管理机制,linux进程管理,linux进程管理机制

    linux进程管理,linux进程管理机制 一.基本介绍 1.在 LINUX 中,每个执行的程序(代码)都称为一个进程.每一个进程都分配一个 ID 号 2.每一个进程,都会对应一个父进程,而这个父进程 ...

  3. Linux—进程管理

    1. 进程的概念 Linux是一个多用户多任务的操作系统.多用户是指多个用户可以在同一时间使用同一个linux系统:多任务是指在Linux下可以同时执行多个任务,更详细的说,linux采用了分时管理的 ...

  4. Linux 进程管理工具

    Linux进程管理命令:     pstree.ps.top.pidof.htop.glances.pmap.vmstat.dstat.kill.pkill.job.bg.fg.nohup.pgrep ...

  5. linux进程管理命令实验,实验2Linux进程管理.doc

    实验2Linux进程管理 实验2 Linux进程管理 实验目的 1.加深对进程概念的理解,明确进程和程序的区别 2.进一步认识并发执行的实质 3.分析进程争用资源的现象,学习解决进程互斥的方法 实验性 ...

  6. linux进程管理fork,Linux -- 进程管理之 fork() 函数

    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间.然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同.相当于克隆了一个自己. Test1 f ...

  7. Linux进程管理之ps的使用

    主题Linux进程管理之ps工具的使用 一ps工具的介绍 ps: process state  进程状态 ps - report a snapshot of the current processes ...

  8. linux ps 进程组,linux进程管理(2)---进程的组织结构

    一.目的 linux为了不同的进程管理目的,使用了不同的方法组织进程之间的关系,为了体现父子关系,使用了"树形"图:为了对同一信号量统一处理,使用了进程组:为了快速查找某个进程,使 ...

  9. linux进程管理命令kill,Linux进程管理命令-kill | IT运维网

    格式:kill -l [signal] -l:显示当前系统可用信号 -l:显示当前系统可用信号 [root@localhost ~]# kill -l 1) SIGHUP 2) SIGINT 3) S ...

  10. linux进程管理子系统分析,linux进程管理子系统简要分析

    Linux进程管理: 进程与程序: 程序:存放在磁盘上的一系列代码和数据的可执行映像,是一个静止的实体. 进程:是一个执行中的程序,它是动态的实体 进程四要素: 1. 有一段程序供其执行,这段程序不一 ...

最新文章

  1. python数据分析pandas_Python数据分析之pandas学习(基础操作)
  2. [转载][QT][SQL]sql学习记录3_sqlite之update delete like
  3. 设计模式(五):命令模式
  4. Django中datetime类型的相关操作(记录一下)
  5. 离线安装PostgreSQL
  6. 一线城市夫妻“双城记”:带板凳上地铁,从江苏坐到江苏路站
  7. 2008 Gartner ITxpo 展 10 大 IT 产品
  8. 使用Visual Studio SDK制作GLSL词法着色插件
  9. 汇编语言 王爽 第四版 实验3
  10. Java与C++的区别
  11. SAP-如何学好SAP BASIS
  12. DS18B20温度传感器学习笔记
  13. MacBook邮件登陆163邮箱,解决无法验证账户名或密码的问题
  14. ABP理论学习之EntityFramework集成
  15. CRC循环冗余校验是如何纠错的
  16. 可以检测手机帧率和温度的软件_让电影动漫统统变丝滑,480帧毫无卡顿,交大博士生开源插帧软件
  17. 操作系统-动态内存分配算法
  18. 基于python的pdf文件处理系统
  19. C++进阶——内存管理(三)
  20. 机器学习——基于M-distance的推荐

热门文章

  1. mysql 数据库事务处理_MySQL数据库事务及其原理
  2. 华为云龙api自动化工具_2020年十大最佳自动化测试工具
  3. kuka程序备份_那智机器人系统备份步骤
  4. burst tx 功能 开启_Serverspeeder 锐速config配置文件详解
  5. avl删除根节点图解_图解 6 种树,你心中有数吗。。。
  6. android获取url参数值,android 获取url 参数值
  7. html框架集把那根框架隐藏,HTML 框架集 frameset 和内嵌框架 iframe
  8. 【机器学习算法专题(蓄力计划)】十三、机器学习中线性回归
  9. 八十七、CSS水平垂直居中的布局方式
  10. 七、SpringBoot整合elasticsearch集群