第 7 章进程控制开发

本章目标
文件是 Linux 中最常见最基础的操作对象,而进程则是系统调度的单位,在上一章学习了文件I/O 控制之后,本章主要讲解进程控制开发部分,通过本章的学习,读者将会掌握以下内容。
  • 掌握进程相关的基本概念
  • 掌握 Linux 下的进程结构
  • 掌握 Linux 下进程创建及进程管理
  • 掌握 Linux下进程创建相关的系统调用
  • 掌握守护进程的概念
  • 掌握守护进程的启动方法
  • 掌握守护进程的输出及建立方法
  • 学会编写多进程程序
  • 学会编写守护进程
7.1 Linux 下进程概述
7.1.1 进程相关基本概念
1.进程的定义
进程的概念首先是在60年代初期由MIT的Multics系统和IBM的TSS/360系统引入的。
经过了40 多年的发展,人们对进程有过各种各样的定义。现列举较为著名的几种。
(1)进程是一个独立的可调度的活动(E. Cohen,D. Jofferson)
(2)进程是一个抽象实体,当它执行某个任务时,将要分配和释放各种资源(P. Denning)
(3)进程是可以并行执行的计算部分。(S. E. Madnick,J. T. Donovan)
以上进程的概念都不相同,但其本质是一样的。它指出了进程是一个程序的一次执行的过程。它和程序是有本质区别的,程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程。它是程序执行和资源管理的最小单位。因此,对系统而言,当用户在系统中键入命令执行一个程序的时候,它将启动一个进程。
2.进程控制块
进程是 Linux 系统的基本调度单位,那么从系统的角度看如何描述并表示它的变化呢?
在这里,是通过进程控制块来描述的。进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述。在Linux 中,进程控制块中的每一项都是一个task_struct结构,它是在include/linux/sched.h中定义的。
3.进程的标识
在 Linux 中最主要的进程标识有进程号(PID,Process Idenity Number)和它的父进程号(PPID,parent process ID)。其中PID 惟一地标识一个进程。PID 和PPID 都是非零的正整数。
在 Linux 中获得当前进程的PID 和PPID 的系统调用函数为getpid和getppid,通常程序获得当前进程的PID 和PPID 可以将其写入日志文件以做备份。getpid和getppid系统调用过程如下所示:
/*process.c*/
#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
int main()
{
/*获得当前进程的进程ID和其父进程ID*/
printf("The PID of this process is %d\n",getpid());
printf("The PPID of this process is %d\n",getppid());
}
使用arm-linux-gcc进行交叉编译,再将其下载到目标板上运行该程序,可以得到如下结果,该值在不同的系统上会有所不同:
[root@localhost process]# ./process
The PID of this process is 78
THe PPID of this process is 36
另外,进程标识还有用户和用户组标识、进程时间、资源利用情况等,这里就不做一一介绍,感兴趣的读者可以参见W.Richard Stevens 编著的《Advanced Programming in the UNIX Environmen》。
4.进程运行的状态
进程是程序的执行过程,根据它的生命期可以划分成3 种状态。
•执行态:该进程正在,即进程正在占用CPU。
•就绪态:进程已经具备执行的一切条件,正在等待分配CPU的处理时间片。
•等待态:进程不能使用CPU,若等待事件发生则可将其唤醒。
它们之间转换的关系图如图7.1 所示。
等待某个事件发生
而睡眠
因等待事件发生
而唤醒
时间片到
执行
就绪
等待
图7.1 进程3种状态的转化关系
7.1.2 Linux下的进程结构
Linux 系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。其中,每一个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。
Linux 中的进程包含3个段,分别为“数据段”、“代码段”和“堆栈段”。
•“数据段”存放的是全局变量、常数以及动态数据分配的数据空间(如malloc 函数取得的空间)等。
•“代码段”存放的是程序代码的数据。
•“堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量。如图7.2 所示。
7.1.3 Linux下进程的模式和类型
在 Linux 系统中,进程的执行模式划分为用户模式和内核模式。如果当前运行的是用户程序、应用程序或者内核之外的系统程序,那么对应进程就在用户模式下运行;如果在用户程序执行过程中出现系统调用或者发生中断事件,那么就要运行操作系统(即核心)程序,进程模式就变成内核模式。在内核模式下运行的进程可以执行机器的特权指令,而且此时该进程的运行不受用户的干扰,即使是root 用户也不能干扰内核模式下进程的运行。
用户进程既可以在用户模式下运行,也可以在内核模式下运行,如图7.3所示。
内核进程
中断或系统调用
用户进程
用户态
内核态
图7.3 用户进程的两种运行模式
7.1.4 Linux下的进程管理
Linux 下的进程管理包括启动进程和调度进程,下面就分别对这两方面进行简要讲解。
1.启动进程
Linux 下启动一个进程有两种主要途径:手工启动和调度启动。手工启动是由用户输入命令直接启动进程,而调度启动是指系统根据用户的设置自行启动进程。
(1)手工启动
手工启动进程又可分为前台启动和后台启动。
•前台启动是手工启动一个进程的最常用方式。一般地,当用户键入一个命令如“ls -l”时,就已经启动了一个进程,并且是一个前台的进程。
代码段数据段堆栈段
图7.2 Linux中进程结构示意图
第7章、进程控制开发
•后台启动往往是在该进程非常耗时,且用户也不急着需要结果的时候启动的。比如用户要启动一个需要长时间运行的格式化文本文件的进程。为了不使整个shell在格式化过程中都处于“瘫痪”状态,从后台启动这个进程是明智的选择。
(2)调度启动
有时,系统需要进行一些比较费时而且占用资源的维护工作,并且这些工作适合在深夜无人职守的时候进行,这时用户就可以事先进行调度安排,指定任务运行的时间或者场合,到时候系统就会自动完成这一切工作。
使用调度启动进程有几个常用的命令,如at命令在指定时刻执行相关进程,cron命令可以自动周期性地执行相关进程,在需要使用时读者可以查看相关帮助手册。
2.调度进程
调度进程包括对进程的中断操作、改变优先级、查看进程状态等,在Linux 下可以使用相关的系统命令实现其操作,下表列出了Linux 中常见的调用进程的系统命令,读者在需要的时候可以自行查找其用法。
表7.1 Linux中进程调度常见命令
选 项 参 数 含 义
Ps 查看系统中的进程
Top 动态显示系统中的进程
Nice 按用户指定的优先级运行
Renice 改变正在运行进程的优先级
Kill 终止进程(包括后台进程)
crontab 用于安装、删除或者列出用于驱动cron后台进程的任务。
Bg 将挂起的进程放到后台执行
7.2 Linux 进程控制编程
进程创建
1.fork()
在 Linux 中创建一个新进程的惟一方法是使用fork函数。fork 函数是Linux 中一个非常重要的函数,和读者以往遇到的函数也有很大的区别,它执行一次却返回两个值。希望读者能认真地学习这一部分的内容。
(1)fork函数说明
fork 函数用于从已存在进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。这两个分别带回它们各自的返回值,其中父进程的返回值是子进程的进程号,而子进程则返回0。因此,可以通过返回值来判定该进程是父进程还是子进程。
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。因此可以看出,使用fork函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork 函数的执行速度并不很快。
(2)fork函数语法
表 7.2 列出了fork函数的语法要点。
表7.2 fork函数语法要点
所需头文件#include <sys/types.h> // 提供类型pid_t 的定义
#include <unistd.h>
函数原型pid_t fork(void)
0:子进程
函数返回值子进程ID(大于0的整数):父进程
1:出错
(3)fork函数使用实例
/*fork.c*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
/*调用fork函数,其返回值为result*/
result = fork();
/*通过result的值来判断fork函数的返回情况,首先进行出错处理*/
if(result  == -1){
perror("fork");
exit;
}
/*返回值为0代表子进程*/
else if(result == 0){
printf("The return value is %d\nIn child process!!\nMy PID is %d\n",result,getpid());
}
/*返回值大于0代表父进程*/
else
{
printf("The return value is %d\nIn father process!!\nMy PID is %d\n",result,getpid());
}
}
[root@localhost process]# arm-linux-gcc fork..c –o fork
将可执行程序下载到目标板上,运行结果如下所示:
The return valud s 76
In father process!!
My PID is 75
The return value is :0
In child process!!
My PID is 76
从该实例中可以看出,使用fork 函数新建了一个子进程,其中的父进程返回子进程的PID,而子进程的返回值为0。
(4)函数使用注意点
fork函数使用一次就创建一个进程,所以若把fork函数放在了if else判断语句中则要小心,不能多次使用fork函数。
小知识
由于 fork 完整地拷贝了父进程的整个地址空间,因此执行速度是比较慢的。为了加快fork 的执行速度,有些UNIX系统设计者创建了vfork。vfork也能创建新进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才拷贝父进程。这就是著名的“写操作时拷贝”(copy-on-write)技术。
现在很多嵌入式Linux 系统的fork 函数调用都采用vfork 函数的实现方式,实际上uClinux所有的多进程管理都通过vfork来实现。
2.exec函数族
(1)exec函数族说明
fork 函数是用于创建一个子进程,该子进程几乎拷贝了父进程的全部内容,但是,这个新创建的进程如何执行呢?这个exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux 下任何可执行的脚本文件。
在 Linux 中使用exec函数族主要有两种情况:
•当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生;
•如果一个进程想执行另一个程序,那么它就可以调用fork 函数新建一个进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程。(这种情况非常普遍)
(2)exec函数族语法
实际上,在Linux 中并没有exec()函数,而是有6 个以exec开头的函数族,它们之间语法有细微差别,本书在下面会详细讲解。
下表7.3 列举了exec函数族的6 个成员函数的语法。
表7.3 exec 函数族成员函数语法
所需头文件#include <unistd.h>
int execl(const char *path, const char *arg, ...)
int execv(const char *path, char *const argv[])
int execle(const char *path, const char *arg, ..., char *const envp[])
int execve(const char *path, char *const argv[], char *const envp[])
int execlp(const char *file, const char *arg, ...)
函数原型
int execvp(const char *file, char *const argv[])
函数返回值1:出错
这 6 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较。
•查找方式
读者可以注意到,表7.3 中的前4 个函数的查找方式都是完整的文件目录路径,而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会