首页 第7章 进程控制开发

第7章 进程控制开发

举报
开通vip

第7章 进程控制开发 《嵌入式 LINUX 应用程序开发标准教程》 作者:华清远见 第 7章 进程控制开发 本章目标 文件是 Linux 中最常见、最基础的操作对象,而进程则是系统资源的单位,本 章主要讲解进程控制开发部分,通过本章的学习,读者将会掌握以下内容。 掌握进程相关的基本概念 掌握 Linux下的进程结构 掌握 Linux下进程创建及进程管理 掌握 Linu...

第7章  进程控制开发
《嵌入式 LINUX 应用程序开发标准教程》 作者:华清远见 第 7章 进程控制开发 本章目标 文件是 Linux 中最常见、最基础的操作对象,而进程则是系统资源的单位,本 章主要讲解进程控制开发部分,通过本章的学习,读者将会掌握以下 内容 财务内部控制制度的内容财务内部控制制度的内容人员招聘与配置的内容项目成本控制的内容消防安全演练内容 。 掌握进程相关的基本概念 掌握 Linux下的进程结构 掌握 Linux下进程创建及进程管理 掌握 Linux下进程创建相关的系统调用 掌握守护进程的概念 掌握守护进程的启动方法 掌握守护进程的输出及建立方法 学会编写多进程程序 学会编写守护进程 专业始于专注 卓识源于远见 ‐ 2 ‐ 7.1 Linux 进程概述 7.1.1 进程的基本概念 1.进程的定义 进程的概念首先是在 20世纪 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()系统调用过程如下所示: /* pid.c */ #include #include #include 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进行交叉编译,再将其下载到目标板上运行该程序,可以得到如下结果,该值在不同的 系统上会有所不同: 专业始于专注 卓识源于远见 ‐ 3 ‐ $ ./pid 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 个段,分别为“数据段”、“代码段”和 “ 堆 栈 段”。  “数据段”存放的是全局变量、常数以及动态数据 分配的数 据空间,根据存放的数据,数据段又可以分成普通 数据段(包 括可读可写/只读数据段,存放静态初始化的全局 变量或常 量)、BSS 数据段(存放未初始化的全局变量)以 及堆(存放 动态分配的数据)。  “代码段”存放的是程序代码的数据。  “堆栈段”存放的是子程序的返回地址、子程序的参 数以及程 序的局部变量等。如图 7.2所示。 7.1.3 Linux下进程的模式和类型 在 Linux系统中,进程的执行模式划分为用户模式和内核模式。如果当前运行的是用户程序、应用程序或者 内核之外的系统程序,那么对应进程就在用户模式下运行;如果在用户程序执行过程中出现系统调用或者发 生中断事件,那么就要运行操作系统(即核心)程序,进程模式就变成内核模式。在内核模式下运行的进程 可以执行机器的特权指令,而且此时该进程的运行不受用户的干扰,即使是 root用户也不能干扰内核模式下 进程的运行。 用户进程既可以在用户模式下运行,也可以在内核模式下运行,如图 7.3所示。 堆栈 堆 数据段(可读/只读) 数据段 代码段 存放传递参数及环境变量 BSS数据段 低地址 高地址 图 7.2 Linux中进程结构示意图 专业始于专注 卓识源于远见 ‐ 4 ‐ 7.1.4 Linux下的进程管理 Linux 下的进程管理包括启动进程和调度进程,下面就 分 别 对 这 两方面进行简要讲解。 1.启动进程 Linux下启动一个进程有两种主要途径:手工启动和调度启动。手工启动是由用户输入命令直接启动进程, 而调度启动是指系统根据用户的设置自行启动进程。 (1)手工启动。 手工启动进程又可分为前台启动和后台启动。  前台启动是手工启动一个进程的最常用方式。一般地,当用户键入一个命令如“ls -l”时,就已经启 动了一个进程,并且是一个前台的进程。  后台启动往往是在该进程非常耗时,且用户也不急着需要结果的时候启动的。比如用户要启动一个需 要长时间运行的格式化文本文件的进程。为了不使整个 shell在格式化过程中都处于“瘫痪”状态,从后台 启动这个进程是明智的选择。 (2)调度启动。 有时,系统需要进行一些比较费时而且占用资源的维护工作,并且这些工作适合在深夜无人值守的时候进 行,这时用户就可以事先进行调度安排,指定任务运行的时间或者场合,到时候系统就会自动完成这一切 工作。 使用调度启动进程有几个常用的命令,如 at命令在指定时刻执行相关进程,cron命令可以自动周期性地执 行相关进程,在需要使用时读者可以查看相关帮助手册。 2.调度进程 调度进程包括对进程的中断操作、改变优先级、查看进程状态等,在 Linux下可以使用相关的系统命令实 现其操作,在表 7.1 中列出了 Linux 中常见的调用进程的系统命令,读者在需要的时候可以自行查找其用 法。 表 7.1 Linux 中进程调度常见命令 选 项 参 数 含 义 ps 查看系统中的进程 top 动态显示系统中的进程 nice 按用户指定的优先级运行 renice 改变正在运行进程的优先级 kill 向进程发送信号(包括后台进程) crontab 用于安装、删除或者列出用于驱动 cron后台进程的任务。 bg 将挂起的进程放到后台执行 用户进程 内核进程 用户态 内核态 中断或系统调用 图 7.3用户进程的两种运行模式 专业始于专注 卓识源于远见 ‐ 5 ‐ 7.2 Linux 进程控制编程 1.fork() 在 Linux中创建一个新进程的惟一方法是使用 fork()函数。fork()函数是 Linux中一个非常重要的函数,和 读者以往遇到的函数有一些区别,因为它看起来执行一次却返回两个值。难道一个函数真的能返回两个值 吗?希望读者能认真地学习这一部分的内容。 (1)fork()函数说明。 fork()函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。使用 fork() 函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、 代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目 录、根 目录 工贸企业有限空间作业目录特种设备作业人员作业种类与目录特种设备作业人员目录1类医疗器械目录高值医用耗材参考目录 、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。 因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序。因此需要用一种方式来区分 它们,并使它们照此运行,否则,这两个进程不可能做不同的事。 实际上是在父进程中执行 fork()函数时,父进程会复制出一个子进程,而且父子进程的代码从 fork()函数的 返回开始分别在两个地址空间中同时运行。从而两个进程分别获得其所属 fork()的返回值,其中在父进程 中的返回值是子进程的进程号,而在子进程中返回 0。因此,可以通过返回值来判定该进程是父进程还是 子进程。 同时可以看出,使用 fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部 分内容,使得 fork()函数的系统开销比较大,而且执行速度也不是很快。 (2)fork()函数语法。 表 7.2列出了 fork()函数的语法要点。 表 7.2 fork()函数语法要点 所需头文件 #include // 提供类型 pid_t的定义 #include 函数原型 pid_t fork(void) 函数返回值 0:子进程 子进程 ID(大于 0的整数):父进程 1:出错 (3)fork()函数使用实例。 /* fork.c */ #include #include #include #include int main(void) { pid_t result; /*调用 fork()函数*/ result = fork(); /*通过 result的值来判断 fork()函数的返回情况,首先进行出错处理*/ 专业始于专注 卓识源于远见 ‐ 6 ‐ if(result == -1) { printf("Fork error\n"); } else if (result == 0) /*返回值为 0代表子进程*/ { printf("The returned value is %d\n In child process!!\nMy PID is %d\n",result,getpid()); } else /*返回值大于 0代表父进程*/ { printf("The returned value is %d\n In father process!!\nMy PID is %d\n",result,getpid()); } return result; } 将可执行程序下载到目标板上,运行结果如下所示: $ arm-linux-gcc fork.c –o fork (或者修改 Makefile) $ ./fork The returned value is 76 /* 在父进程中打印的信息 */ In father process!! My PID is 75 The returned value is :0 /* 在子进程中打印的信息 */ In child process!! My PID is 76 从该实例中可以看出,使用 fork()函数新建了一个子进程,其中的父进程返回子进程的 PID,而子进程的 返回值为 0。 (4)函数使用注意点。 fork()函数使用一次就创建一个进程,所以若把 fork()函数放在了 if else判断语句中则要小心,不能多次使 用 fork()函数。 小知识 由于 fork()完整地复制了父进程的整个地址空间,因此执行速度是比较慢的。为 了加快 fork()的执行速度,有些 UNIX 系统 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 者创建了 vfork()。vfork()也能创 建新进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内 存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才复 制父进程。这就是著名的“写操作时复制”(copy-on-write)技术。 现在很多嵌入式 Linux系统的 fork()函数调用都采用 vfork()函数的实现方式,实 际上 uClinux所有的多进程管理都通过 vfork()来实现。 2.exec函数族 (1)exec函数族说明。 fork()函数是用于创建一个子进程,该子进程几乎复制了父进程的全部内容,但是,这个新创建的进程如何 执行呢?这个 exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或 目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进 专业始于专注 卓识源于远见 ‐ 7 ‐ 程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也 可以是 Linux下任何可执行的脚本文件。 在 Linux中使用 exec函数族主要有两种情况。  当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec函数族中的任意一个函数 让自己重生。  如果一个进程想执行另一个程序,那么它就可以调用 fork()函数新建一个进程,然后调用 exec函 数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常 普遍)。 (2)exec函数族语法。 实际上,在 Linux中并没有 exec()函数,而是有 6个以 exec开头的函数,它们之间语法有细微差别,本书 在下面会详细讲解。 下表 7.3列举了 exec函数族的 6个成员函数的语法。 表 7.3 exec 函数族成员函数语法 所需头文件 #include 函数原型 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结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“$PATH”所指定的路径进行查找。  参数传递方式。 exec函数族的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组 传递。 在这里是以函数名的第 5位字母来区分的,字母为“l”(list)的表示逐个列举参数的方式,其语法为 char *arg;字母为“v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为*const argv[]。读者可以 观察 execl()、execle()、execlp()的语法与 execv()、execve()、execvp()的区别。它们具体的用法在后面的实 例讲解中会具体说明。 这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令 本身)。要注意的是,这些参数必须以 NULL 表示结束,如果使用逐个列举方式,那么要把它强制转化成 一个字符指针,否则 exec将会把它解释为一个整型参数,如果一个整型数的长度 char *的长度不同,那么 exec函数就会报错。  环境变量。 exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两 个函数 execle()和 execve()就可以在 envp[]中指定当前进程所使用的环境变量。 表 7.4是对这 4个函数中函数名和对应语法的小结,主要指出了函数名中每一位所表明的含义,希望读者 结合此表加以记忆。 表 7.4 exec 函数名对应含义 前 4位 统一为:exec 第 5位 l:参数传递为逐个列举方式 execl、execle、execlp 专业始于专注 卓识源于远见 ‐ 8 ‐ v:参数传递为构造指针数组方式 execv、execve、execvp 第 6位 e:可传递新进程环境变量 execle、execve p:可执行文件查找方式为文件名 execlp、execvp (3)exec使用实例。 下面的第一个示例说明了如何使用文件名的方式来查找可执行文件,同时使用参数列表的方式。这里用的 函数是 execlp()。 /*execlp.c*/ #include #include #include int main() { if (fork() == 0) { /*调用 execlp()函数,这里相当于调用了"ps -ef"命令*/ if ((ret = execlp("ps", "ps", "-ef", NULL)) < 0) { printf("Execlp error\n"); } } } 在该程序中,首先使用 fork()函数创建一个子进程,然后在子进程里使用 execlp()函数。读者可以看到,这 里的参数列表列出了在 shell中使用的命令名和选项。并且当使用文件名进行查找时,系统会在默认的环境 变量 PATH中寻找该可执行文件。读者可将编译后的结果下载到目标板上,运行结果如下所示: $ ./execlp PID TTY Uid Size State Command 1 root 1832 S init 2 root 0 S [keventd] 3 root 0 S [ksoftirqd_CPU0] 4 root 0 S [kswapd] 5 root 0 S [bdflush] 6 root 0 S [kupdated] 7 root 0 S [mtdblockd] 8 root 0 S [khubd] 35 root 2104 S /bin/bash /usr/etc/rc.local 36 root 2324 S /bin/bash 41 root 1364 S /sbin/inetd 53 root 14260 S /Qtopia/qtopia-free-1.7.0/bin/qpe -qws 54 root 11672 S quicklauncher 65 root 0 S [usb-storage-0] 66 root 0 S [scsi_eh_0] 83 root 2020 R ps -ef $ env …… 专业始于专注 卓识源于远见 ‐ 9 ‐ PATH=/Qtopia/qtopia-free-1.7.0/bin:/usr/bin:/bin:/usr/sbin:/sbin …… 此程序的运行结果与在 shell中直接键入命令“ps -ef”是一样的,当然,在不同系统的不同时刻都可能会 有不同的结果。 接下来的示例使用完整的文件目录来查找对应的可执行文件。注意目录必须以“/”开头,否则将其视为文 件名。 /*execl.c*/ #include #include #include int main() { if (fork() == 0) { /*调用 execl()函数,注意这里要给出 ps程序所在的完整路径*/ if (execl("/bin/ps","ps","-ef",NULL) < 0) { printf("Execl error\n"); } } } 同样下载到目标板上运行,运行结果同上例。 下面的示例利用函数 execle(),将环境变量添加到新建的子进程中,这里的“env”是查看当前进程环境变 量的命令,如下所示: /* execle.c */ #include #include #include int main() { /*命令参数列表,必须以 NULL结尾*/ char *envp[]={"PATH=/tmp","USER=david", NULL}; if (fork() == 0) { /*调用 execle()函数,注意这里也要指出 env的完整路径*/ if (execle("/usr/bin/env", "env", NULL, envp) < 0) { printf("Execle error\n"); } } } 下载到目标板后的运行结果如下所示: 专业始于专注 卓识源于远见 ‐ 10 ‐ $./execle PATH=/tmp USER=sunq 最后一个示例使用 execve()函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以 NULL作 为结尾标识符。其代码和运行结果如下所示: #include #include #include int main() { /*命令参数列表,必须以 NULL结尾*/ char *arg[] = {"env", NULL}; char *envp[] = {"PATH=/tmp", "USER=david", NULL}; if (fork() == 0) { if (execve("/usr/bin/env", arg, envp) < 0) { printf("Execve error\n"); } } } 下载到目标板后的运行结果如下所示: $ ./execve PATH=/tmp USER=david (4)exec函数族使用注意点。 在使用 exec函数族时,一定要加上错误判断语句。exec很容易执行失败,其中最常见的原因有:  找不到文件或路径,此时 errno被设置为 ENOENT;  数组 argv和 envp忘记用 NULL结束,此时 errno被设置为 EFAULT;  没有对应可执行文件的运行权限,此时 errno被设置为 EACCES。 小知识 事实上,这 6个函数中真正的系统调用只有 execve(),其他 5个都是库函数,它们最终都会调用 execve()这个系统调用。 3.exit()和_exit() (1)exit()和_exit()函数说明。 exit()和_exit()函数都是用来终止进程的。当程序执行到 exit()或_exit()时,进程会无条件地停止剩下的所有 操作,清除包括 PCB在内的各种数据结构,并终止本进程 的运行。但 是,这两个函数还是有区别的,这两个函数的调用过程如 图 7.4 所 示。 从图中可以看出,_exit()函数的作用是:直接使进程停止运行, 清除其使用 的内存空间,并清除其在内核中的各种数据结构;exit()函数则 在这些基础 进程运行 进程终止运行 调用exit系统调用 调用退出处理函数 清理I/O缓冲 exit() _exit() 图 7.4 exit和_exit函数流程图 专业始于专注 卓识源于远见 ‐ 11 ‐ 上做了一些包装,在执行退出之前加了若干道工序。exit()函数与_exit()函数最大的区别就在于 exit()函数在调用 exit系统之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理 I/O缓冲”一项。 由于在 Linux的标准函数库中,有一种被称作“缓冲 I/O(buffered I/O)”操作,其特征就是对应每一个打 开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条 记录 混凝土 养护记录下载土方回填监理旁站记录免费下载集备记录下载集备记录下载集备记录下载 ,这样在下次读文件时就可 以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定 的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。 这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据,认为已经被写入文件 中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭, 缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定要使用 exit()函数。 (2)exit()和_exit()函数语法。 表 7.5列出了 exit()和_exit()函数的语法规范。 表 7.5 exit()和_exit()函数族语法 所需头文件 exit:#include _exit:#include 函数原型 exit:void exit(int status) _exit:void _exit(int status) 函数传入值 status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说, 0表示正常结束;其他的数值表示出现了错误,进程非正常结束。 在实际编程时,可以用 wait()系统调用接收子进程的返回值,从而针对不同的情况 进行不同的处理 (3)exit()和_exit()使用实例。 这两个示例比较了 exit()和_exit()两个函数的区别。由于 printf()函数使用的是缓冲 I/O方式,该函数在遇到 “\n”换行符时自动从缓冲区中将记录读出。示例中就是利用这个性质来进行比较的。以下是示例 1 的代 码: /* exit.c */ #include #include int main() { printf("Using exit...\n"); printf("This is the content in buffer"); exit(0); } $./exit Using exit... This is the content in buffer $ 读者从输出的结果中可以看到,调用 exit()函数时,缓冲区中的记录也能正常输出。 以下是示例 2的代码: /* _exit.c */ #include #include int main() { 专业始于专注 卓识源于远见 ‐ 12 ‐ printf("Using _exit...\n"); printf("This is the content in buffer"); /* 加上回车符之后结果又如何 */ _exit(0); } $ ./_exit Using _exit... $ 读者从最后的结果中可以看到,调用_exit()函数无法输出缓冲区中的记录。 小知识 在一个进程调用了 exit()之后,该进程并不会立刻完全消失,而是留下一个称为 僵尸进程(Zombie)的数据结构。僵尸进程是一种非常特殊的进程,它已经放弃 了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表 中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵 尸进程不再占有任何内存空间。 4.wait()和 waitpid() (1)wait()和 waitpid()函数说明。 wait()函数是用于使父进程(也就是调用 wait()的进程)阻塞,直到一个子进程结束或者该进程接到了一个 指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则 wait()就会立即返回。 waitpid()的作用和 wait()一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个 非阻塞版本的 wait()功能,也能支持作业控制。实际上 wait()函数只是 waitpid()函数的一个特例,在 Linux 内部实现 wait()函数时直接调用的就是 waitpid()函数。 (2)wait()和 waitpid()函数格式说明。 表 7.6列出了 wait()函数的语法规范。 表 7.6 wait()函数族语法 所需头文件 #include #include 函数原型 pid_t wait(int *status) 函数传入值 这里的 status是一个整型指针,是该子进程退出时的状态  status若不为空,则通过它可以获得子进程的结束状态 另外,子进程的结束状态可由 Linux中一些特定的宏来测定 函数返回值 成功:已结束运行的子进程的进程号 失败:1 表 7.7列出了 waitpid()函数的语法规范。 表 7.7 waitpid()函数语法 所需头文件 #include #include 函数原型 pid_t waitpid(pid_t pid, int *status, int options) 专业始于专注 卓识源于远见 ‐ 13 ‐ 续表 函数传入值 Pid pid > 0:只等待进程 ID等于 pid的子进程,不管已经有其他子进程运行 结束退出了,只要指定的子进程还没有结束,waitpid()就会一直等下去 pid = 1:等待任何一个子进程退出,此时和 wait()作用一样 pid = 0:等待其组 ID等于调用进程的组 ID的任一子进程 pid < 1:等待其组 ID等于 pid的绝对值的任一子进程 status 同 wait() options WNOHANG:若由 pid指定的子进程不立即可用,则 waitpid()不阻塞, 此时返回值为 0 WUNTRACED:若实现某支持作业控制,则由 pid指定的任一子进程 状态已暂停,且其状态自暂停以来还未 报告 软件系统测试报告下载sgs报告如何下载关于路面塌陷情况报告535n,sgs报告怎么下载竣工报告下载 过,则返回其状态 0:同 wait(),阻塞父进程,等待子进程退出 函数返回值 正常:已经结束运行的子进程的进程号 使用选项WNOHANG且没有子进程退出:0 调用出错:1 3)waitpid()使用实例。 由于 wait()函数的使用较为简单,在此仅以 waitpid()为例进行讲解。本例中首先使用 fork()创建一个子进程, 然后让其子进程暂停 5s(使用了 sleep()函数)。接下来对原有的父进程使用 waitpid()函数,并使用参数 WNOHANG 使该父进程不会阻塞。若有子进程退出,则 waitpid()返回子进程号;若没有子进程退出,则 waitpid()返回 0,并且父进程每隔一秒循环判断一次。该程序的流程图如图 7.5所示。 开始 fork() fork()返回值 父进程调用 waitpid() 子进程暂停5s 子进程退出 waitpid() 返回值 父进程暂停5s 捕获子进程退出 结束 返回值 = 0 返回值>0 返回值<0(出错) 返回值 = 0 返回值 = 子进程号 图 7.5 waitpid示例程序流 该程序源代码如下所示: /* waitpid.c */ #include #include #include #include #include int main() { pid_t pc, pr; pc = fork(); 专业始于专注 卓识源于远见 ‐ 14 ‐ if (pc < 0) { printf("Error fork\n"); } else if (pc == 0) /*子进程*/ { /*子进程暂停 5s*/ sleep(5); /*子进程正常退出*/ exit(0); } else /*父进程*/ { /*循环测试子进程是否退出*/ do { /*调用 waitpid,且父进程不阻塞*/ pr = waitpid(pc, NULL, WNOHANG); /*若子进程还未退出,则父进程暂停 1s*/ if (pr == 0) { printf("The child process has not exited\n"); sleep(1); } } while (pr == 0); /*若发现子进程退出,打印出相应情况*/ if (pr == pc) { printf("Get child exit code: %d\n",pr); } else { printf("Some error occured.\n"); } } } 将该程序交叉编译,下载到目标板后的运行结果如下所示: $./waitpid The child process has not exited The child process has not exited The child process has not exited The child process has not exited The child process has not exited Get child exit code: 75 可见,该程序在经过 5次循环之后,捕获到了子进程的退出信号,具体的子进程号在不同的系统上会有所 专业始于专注 卓识源于远见 ‐ 15 ‐ 区别。 读者还可以尝试把“pr = waitpid(pc, NULL, WNOHANG);”这句改为“pr = waitpid(pc, NULL, 0);”或者“pr = wait(NULL);”,运行的结果为: $./waitpid Get child exit code: 76 可见,在上述两种情况下,父进程在调用 waitpid()或 wait()之后就将自己阻塞,直到有子进程退出为止。 7.3 Linux 守护进程 7.3.1 守护进程概述 守护进程,也就是通常所说的 Daemon进程,是 Linux中的后台服务进程。它是一个生存期较长的进程, 通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导载 入时启动,在系统关闭时终止。Linux 有很多系统服务,大多数服务都是通过守护进程实现的,如本书在 第二章中讲到的多种系统服务都是守护进程。同时,守护进程还能完成许多系统任务,例如,作业规划进 程 crond、打印进程 lqd等(这里的结尾字母 d就是 Daemon的意思)。 由于在 Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附 于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但 是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才会退出。如果想让某个进 程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。可见,守护 进程是非常重要的。 7.3.2 编写守护进程 编写守护进程看似复杂,但实际上也是遵循一个特定的流程。只要将此流程掌握了,就能很方便地编写出用 户自己的守护进程。下面就分 4个步骤来讲解怎样创建一个简单的守护进程。在讲解的同时,会配合介绍与 创建守护进程相关的几个系统函数,希望读者能很好地掌握。 1.创建子进程,父进程退出 这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在 shell终端里造 成一种程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 shell终端里则可以执行其 他的命令,从而在形式上做到了与控制终端的脱离。 到这里,有心的读者可能会问,父进程创建了子进程之后退出,此时该子进程不就没有父进程了吗?守护 进程中确实会出现这么一个有趣的现象,由于父进程已经先于子进程退出,会造成子进程没有父进程,从 而变成一个孤儿进程。在 Linux中,每当系统发现一个孤儿进程,就会自动由 1号进程(也就是 init进程) 收养它,这样,原先的子进程就会变成 init进程的子进程了。其关键代码如下所示: pid = fork(); if (pid > 0) { exit(0); /*父进程退出*/ } 专业始于专注 卓识源于远见 ‐ 16 ‐ 2.在子进程中创建新会话 这个步骤是创建守护进程中最重要的一步,虽然它的实现非常简单,但它的意义却非常重大。在这里使用 的是系统函数 setsid(),在具体介绍 setsid()之前,读者首先要了解两个概念:进程组和会话期。  进程组。 进程组是一个或多个进程的集合。进程组由进程组 ID 来惟一标识。除了进程号(PID)之外,进程组 ID 也是一个进程的必备属性。 每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID。且该进程 ID不会因组长进程 的退 出而受到影响。  会话期 会话组是一个或多个进程组的集合。通常,一个会话开 始于用户登 录,终止于用户退出,在此期间该用户运行的所有进程 都属于这个 会话期,它们之间的关系如图 7.6所示。 接下来就可以具体介绍 setsid()的相关内容。 (1)setsid()函数作用。 setsid()函数用于创建一个新的会话,并担任该会话组的 组长。调用 setsid()有下面的 3个作用。  让进程摆脱原会话的控制。  让进程摆脱原进程组的控制。  让进程摆脱原控制终端的控制。 那么,在创建守护进程时为什么要调用 setsid()函数呢?读者可以回忆一下创建守护进程的第一步,在那里 调用了 fork()函数来创建子进程再令父进程退出。由于在调用 fork()函数时,子进程全盘复制了父进程的会 话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因 此,还不是真正意义上的独立,而 setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。 (2)setsid()函数格式。 表 7.8列出了 setsid()函数的语法规范。 表 7.8 setsid()函数语法 所需头文件 #include #include 函数原型 pid_t setsid(void) 函数返回值 成功:该进程组 ID 出错:1 3.改变当前目录为根目录 这一步也是必要的步骤。使用 fork()创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中, 当前目录所在的文件系统(比如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(比 如系统由于某种原因要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,
本文档为【第7章 进程控制开发】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_562526
暂无简介~
格式:pdf
大小:647KB
软件:PDF阅读器
页数:0
分类:互联网
上传时间:2013-09-28
浏览量:13