您的位置:首页 > 产品设计 > UI/UE

APUE笔记---第八章 进程控制(函数fork、exit、wait等)

2016-11-10 22:48 459 查看

APUE笔记—第八章 进程控制

1. 进程标识

每一个进程都有一个非负整数作为唯一进程ID。所以应用程序有时把进程ID作为名字的一部分来创建一个唯一的文件名。

虽然进程是唯一的,但是进程ID是可复用的

进程ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,不执行任何磁盘上的程序,也称为系统进程

进程ID为1通常是init进程,在自举过程结束时由内核调用。此进程负责在自举内核后启动一个Unix系统。init进程通常读取与系统有关的初始化文件,并将系统引导到一个状态。init进不会终止,它是一个普通用户进程,但是以超级用户权限运行。

进程ID为2的是页守护进程,此进程负责支持虚拟存储系统的分页操作。

除了进程ID,每个进程还有一些其他的标识符,下列函数可以返回这些标识符。

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
//返回值:调用进程的ID
pid_t getppid(void);
//返回值:调用进程的父ID
uid_t getuid(void);
//返回值:调用进程的实际用户ID
uid_t geteuid(void);
//返回值:调用进程的有效用户ID
gid_t getgid(void);
//返回值:调用进程的实际组ID
gid_t getegid(void);
//返回值:调用进程的有效组ID


以上函数都没有出错返回。

2. 函数fork

函数fork可以创建一个新进程。

#include <unistd.h>

pid_t fork(void);
//返回值:子进程返回0,父进程返回子进程的ID,若出错,返回-1


由fork创建的进程称为子进程(child process),fork调用一次返回两次,子进程返回0,父进程返回子进程的ID。

将子进程的ID返回给父进程的理由:一个进程的子进程可以有多个,但是没有函数可以使进程获得所有子进程的进程ID。

使子进程返回0的理由是:一个进程只有一个父进程,所以子进程总是可以调用getppid函数获得父进程的进程ID。前面说到,进程ID为0是由内核交换进程使用,所以一个子进程的进程ID不可能为0。

子进程和父进程继续执行fork调用后的指令。子进程是父进程的副本。子进程获得父进程的数据空间、堆、栈的副本。子进程和父进程共享文本段,但是子进程和父进程不共享存储空间。

由于fork之后经常跟着exec,所以使用写时复制(Copy-On-Write,COW)的技术。如果父进程和子进程中的人一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统的一“页”。

2.1 fork函数示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int globvar = 10;
char buf[] = "a write to stdout\n";

int main()
{
int var = 20;
pid_t pid;

if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1){//写sizeof(buf)-1长度是为了不输出buf最后的‘\0’。strlen可以计算不带终止‘\0’字节的字符串,但是strlen是函数,需要一次调用,sizeof是关键字,在编译的时候就计算长度。
perror("write err");
exit(1);
}
printf("fork before\n");//不刷新缓冲区
//      fflush(fdopen(STDOUT_FILENO, "r"));

if((pid = fork()) == -1){//创建一个进程
perror("fork err\n");
exit(1);
}else if(pid == 0){//子进程,更改变量
globvar++;
var++;
}else{//父进程,睡眠3秒
sleep(3);
}

printf("pid = %d\tvar = %d\tglobvar = %d\n", getpid(), var, globvar);

return 0;
}


运行结果为:

➜  process_ctl ./a.out
a write to stdout
fork before
pid = 12668 var = 21    globvar = 11    //子进程的值发生改变
pid = 12667 var = 20    globvar = 10    //父进程的值没有改变
➜  process_ctl ./a.out > file
➜  process_ctl cat file
a write to stdout
fork before
pid = 12674 var = 21    globvar = 11
fork before
pid = 12673 var = 20    globvar = 10


fork函数之后父进程先执行还是子进程先执行是不确定的。

第一次执行a.out的时候:因为终端设备是行缓冲的,遇到换行符时会清洗缓冲区,所以只打印了一次fork before到屏幕。

当将标准输出重定向到一个文件时,打印了两次fork before,是因为,若不是终端设备的流实施的是全缓冲机制,在fork之前调用了一次printf,但是当调用fork后,该行数据仍然在缓冲区里,然后子进程复制这些数据,在程序结束时,缓冲区的内容都写到文件中。

61行可以调用fflush函数清洗缓冲区数据到内核。

关于缓冲的知识—APUE第五章的总结

2.2 文件共享

fork的一个特性就是父进程的所有打开文件描述符都被复制到子进程中。就好像对一个文件描述符执行了dup函数一样。父进程和子进程每个相同的打开描述符共享一个文件表项。重要的一定是,父进程和子进程共享同一文件的偏移量。

在fork之后处理文件描述符有以下两种情况:

父进程等待子进程完成。这种情况下,父进程无需对描述符进行处理但是文件的偏移量已经更新。

父进程和子进程各自执行不同的程序段。父进程和子进程各自关闭不使用的文件描述符,用来减少干扰。网络服务进程经常使用。

fork失败的两个主要原因:

系统中已经有了太多的进程。

该实际用户ID的进程数超过了系统限制。

fork的两种用法:

一个父进程希望复制自己,使子进程和父进程同时执行不同的代码段。例如网络服务进程中,父进程等待客户端服务请求,当请求到达,父进程调用fork使子进程处理请求,父进程继续等待下一个服务请求。

一个进程要执行一个不同的程序。fork后立即使用exec。

3.函数vfork

vfork函数的调用序列和返回值与fork相同,但两者语义不同。

两者都会创建一个子进程,但是它并不将父进程的地址空间的完全复制到子进程中因为子进程会立即调用exec或exit,所以它会优化在父进程的空间中运行。

vfork函数保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int globvar = 10;

int main()
{
int var = 20;
pid_t pid;

printf("fork before\n");
if((pid = vfork()) < 0){//创建一个进程
perror("fork err\n");
exit(1);
}else if(pid == 0){//子进程,更改变量
globvar++;
var++;
_exit(0);
}

printf("pid = %d\tvar = %d\tglobvar = %d\n", getpid(), var, globvar);

exit(0);
}


运行结果:

➜  process_ctl ./a.out
fork before
pid = 14845 var = 21    globvar = 11    //父进程打印
//子进程对变量增1,改变了父进程中的变量。因为子进程在父进程的空间运行。


用于fork后马上调用exec函数

父子进程,共用同一地址空间,子进程如果没有马上exec而是修改了父进程出得到的变量值,此修改会在父进程中生效

设计初衷,提高系统效率,减少不必要的开销

现在fork已经具备读时共享写时复制机制,vfork逐渐废弃

4. 函数exit

进程一共有5种正常终止和3种异常终止。无论进程如何终止,最终内核都会执行同一段代码,这段代码为相应进程关闭所有打开的描述符,释放它使用的储存器。

在进程正常终止下,也就是调用exit,_exit,_Exit这三个终止函数,将其退出状态作为参数传递给函数。

在异常终止情况,内核产生一个指示其异常终止原因的终止状态,在该进程的父进程可用wait或waitpid函数取得终止状态。

如果父进程在子进程之前终止:对于这类进程,他们的父进程都改变为init进程,称这些进程由init进程收养。

如果一个进程已经终止,但是父进程尚未对其进行善后处理则称该进程为僵死进程(zombie)。

产生一个僵尸进程:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
pid_t pid;

if((pid = fork()) < 0){
perror("fork err");
exit(1);
}else if(pid == 0){
printf("running in child process\n");
}else{
printf("running in parent process\n");
sleep(10);
}

return 0;
}


运行结果:

➜  process_ctl ./a.out
running in parent process
running in child process
//执行后父进程会睡眠10s,然后用ps aux命令查看进程
3014  3286  3286  3014 pts/4     3286 S+    1000   0:00 ./a.out
3286  3287  3286  3014 pts/4     3286 Z+    1000   0:00 [a.out] <defunct>
//ps命令将僵尸进程的状态打印为Z。


5. 函数wait和waitpid

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);
//返回值:若成功,返回进程ID,若出错,返回-1或0


这两个函数的参数status是一个整型指针,如果status不是空指针,则终止进程的终止状态就存放在它所指向的单元,如果不关系终止状态,则可设定status为NULL。

调用waitpid和wait函数会发生什么?

如果其所有子进程都还在运行,则阻塞。

如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。

如果它没有任何子进程,则立即出错返回。

wait和waitpid函数的区别:

在一个子进程终止前,wait调用其调用者组摄,而waitpid有一选项(options)可使调用者不阻塞。

waitpid并不等待在其调用之后的第一个终止子进程,它由若干个选项,可以控制它所等待的进程。

如果调用者阻塞而且有多个子进程,则在其某一子进程终止时,wait就返回。因为wait返回终止子进程的进程ID。

如果要实现等待一个特定进程的函数,waitpid提供这种功能。waitpid中pid参数的作用解释如下:

pid == -1。等待任一子进程。此情况与wait等效。

pid > 0 。等待ID与pid相等的子进程。

pid == 0 。等待组ID调用进程组ID的任一子进程。

pid < -1 。等待组ID等于pid绝对值的任一子进程。

waitpid函数返回终止子进程的ID,并将该子进程的终止状态放在由status指向的存储单元里。

当options为WNOHANG:若由pid制定的子进程并不是立即返回的,则waitpid不阻塞,此时返回值为0。

实例

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid;

if((pid = fork()) < 0){         /*fork fail*/
perror("first fork err");
exit(1);
}else if(pid == 0){     /*first child*/
if((pid = fork()) < 0){
perror("second fork err");
exit(1);
}else if(pid > 0){      /*parent for second child == first child*/
exit(0);
}
sleep(2);       /*second child*/
printf("second child,parent id = %ld\n", (long)getppid());
exit(0);
}

if(waitpid(pid, NULL, 0) != pid){
perror("waitpid err");
exit(1);
}

exit(0);
}


运行结果

➜   ./a.out
➜   second child,parent id = 1


如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态知道父进程终止,那就就fork两次,将第二个子进程被init进程(ID为1)收养,因为init被编写为无论何时只要一个子进程终止,init就会调用wait函数取得其终止状态,避免该进程塞满僵尸进程。

6. 函数waitid

#include <sys/types.h>
#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
//若成功,返回0,若出错,返回-1


waitid与waitpid类似,waitid允许一个进程指定要等待的子进程,但是它使用两个单独的参数标示等待的子进程所属的类型。

idtype类型如下:

|常量|说明|

|:—:|:—:|

|P_PID|等待一特定的进程:id包含要等待子进程的进程ID|

|P_PGID|等待一特定进程组中的任一子进程:id包含要等待子进程的进程组ID|

|P_ALL|等待任一子进程:忽略id|

当options为常量WNOHANG时,如无可用的子进程退出状态,立即返回而非阻塞。

infop是一个指向siginfo结构的指针,该结构包含信号产生原因的有关信息。

7. 函数wait3和wait4

#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>

pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
//返回值:若成功,返回进程ID,若出错,返回-1


这两个函数提供的功能比之前的函数多一个,这与附加参数有关。该参数允许内核返回终止进程及其所有子进程使用的资源概况,包括:用户CPU总量,系统CPU时间总量,缺页次数,接受信号的次数等。

8. 竞争条件

当进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序是,我们认为发生了竞争条件。如果fork之后的某种逻辑显示或隐示的依赖于在fork之后是父进程还是子进程先运行,那么fork函数就会是竞争条件活跃的滋生地。

如果进程希望等待一个子函数终止,就必须调用wait函数中的一个。如果一个进程要等待其父进程终止,则可使用下列形式的循环:

while(getppid() != 1)
sleep(1);


这种循环称为轮询(polling),它的问题是浪费了CPU的时间。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void charatatime(char *str)
{
char *ptr = str;
int c;

setbuf(stdout, NULL);   //设置标准输出无缓冲

while((c = *ptr++) != 0){
putc(c, stdout);
}
}

int main()
{
pid_t pid;

if((pid = fork()) < 0){
perror("fork err");
exit(1);
}else if(pid == 0){
charatatime("output from child\n");
}else{
charatatime("output from parent\n");
}

return 0;
}


在程序中设置标准输出不带缓冲,于是每个字符输出都需要调用一次write,因为两个进程都需要输出字符串,所以就会形成竞争条件。

运行结果:

➜  process_ctl ./a.out
output from parento
utput from child
➜  process_ctl ./a.out
output from parent
output from child
➜  process_ctl ./a.out
output from parent
output from child
➜  process_ctl ./a.out
output ofurtopmu tpar efnrto
m child


结果看出:有时会错误的输出,有时正确输出,但正确输出不代表竞争条件不存在。

因为进程间可能存在竞争,所以就应该进行通信。

9. 函数exec

当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序从其main函数开始执行,因为调用exec并不创建新进程,所以以后的进程ID并未改变。exec只是用磁盘上的一个新程序替换当前进程的代码段,数据段,堆段,栈段。

fork可以创建新进程,exec可以初始化新进程,exit函数和wait函数处理终止和等待终止,这些是我们需要的基本的进程控制原语。

#include <unistd.h>

int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
//返回自:若出错,返回-1, 若成功,不返回。


这些函数都属于exec函数族,函数名的后缀有四种,分别是l、p、v、和e。

后缀有 l(list): 函数传参方式是命令行参数列表

后缀有p(path): 搜索file时使用PATH环境变量

后缀有v(vector): 函数传参方式是传命令行参数数组的地址argv

后缀有e(environment): 使用环境变量数组envp[],不使用进程原有的环境变量,设置新加载程序运行的环境变量。

函数exec实例

#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
char *ps_argv[] = {"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};           //命令行参数的指针数组
char *ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};     //设置环境变量的指针数组

pid_t pid;

if((pid = fork()) < 0){
perror("fork err");
exit(1);
}else if(pid == 0){
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);   //这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错
则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
perror("execl err");
}else {
//              waitpid(pid, NULL, WNOHANG);//非阻塞等待
wait(NULL); //阻塞等待
}

return 0;


运行结果:

process_ctl ./a.out
PID  PPID  PGRP  SESS TPGID COMMAND
2195  2190  2195  2195  6877 zsh
6877  2195  6877  2195  6877 a.out
6878  6877  6877  2195  6877 ps


对于execl函数,带有字母l,所以第一个参数:传递ps的路径,包含“/”视为路径名,从第二个参数到NULL参数就是命令行参数,可变长参数读到NULL停止,说明要执行的命令和命令选项读完。

下面列出其他函数的表示形式:

execv("/bin/ps", ps_argv);
//带有字母v,所以传递命令的路径和命令行参数的数组地址,字母v和l相互互斥
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
//带有字母l和e,在execl基础上加上新设置的环境变量数组地址作为参数
execve("/bin/ps", ps_argv, ps_envp);
//有字母v和e,传递两个数组地址
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
//有l和p,因为有p,就代表默认用PATH环境变量,所以传递ps命令,它会根据环境变量表的PATH路径寻找ps文件。
execvp("ps", ps_argv);


10. 更改用户ID和更改组ID

一般而言,在设计应用时,我们总是试图使用最小特权模型,依照次模型我们的程序应当只具有完成给定任务所需的最小特权。

可以用setuid和setgid设置实际用户ID和有效用户ID,setgid类似。

#include <sys/types.h>
#include <unistd.h>

int setuid(uid_t uid);
int setgid(gid_t gid);
//返回值:若成功,返回0;若出错,返回-1


若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID和保存的设置用户ID设置为uid。

若进程没有超级用户特权,但uid等于实际用户ID或保存的设置用户ID,则只将有效用户ID设置为uid。

若果以上条件不满足,则error设置为EPERM,返回-1.

11. 函数system

system函数可以在程序中执行一个命令字符串,但是该操作对操作系统的依赖性很强。

#include <stdlib.h>

int system(const char *command);


如果command是一个空指针,返回一个非0值。

因为system的实现调用了fork、exec和waitpid,因此有三个返回值:

fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并设置errno以指示错误类型

如果exec失败,相当与shell执行exit(127),因为标示不能执行shell

三个函数都成功,那么system的返回值是shell的终止状态。

如果一个进程以特殊的权限,设置用户ID或设置组ID运行,该进程又要生成另一个进程执行另一个程序,则应该直接使用执行fork和exec,而且在fork和exec之后更改会普通权限。设置用户ID或设置ID程序绝不应该调用system函数。这是一个安全性方面的漏洞。

12. 进程会计

当启用进程会计(process accounting),每当进程结束时内核就会写一个会计记录,包括命令名,使用CPU时间总量,用户ID和组ID,启动时间。

会计记录所需要的各个数据都由内核保存在进程表中,并在一个新进程被创建时初始化,进程终止时写一个会计记录。

13. 用户标识

getpwuid或getuid函数可以找到运行该程序用户的登录名。但是如果一个用户有多个登录名,这些登录名对应同一个用户ID,函数getlogin可以获取此登录名。

#include <unistd.h>

char *getlogin(void);
//返回值:若成功,返回指向登录名字符串的指针,若出错,返回NULL


如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败,通常称这些函数为守护进程(daemon),得知登录名,就可以用getpwnam在口令文件中找到用户的相应记录,从而确定其登录shell等。

14. 进程调用

进程 可以调整nice值选择以更低优先级运行,只有特权进程允许提高进程调度权限。进程可以通过nice函数获取后更改它的nice值

#include <unistd.h>

int nice(int inc);
//返回值:若成功,返回新的nice值,若出错,返回-1


inc参数被增加到调用进程的nice值上,不能太大和太小。

getpriority函数可以像nice函数一样用于获取进程的nice值,但是还可以获取一组相关进程的nice值,setpriority函数可用于为进程进程组和属于特定用户ID的所有进程设置优先级。

#include <sys/time.h>
#include <sys/resource.h>

int getpriority(int which, id_t who);
//返回值:若成功,返回nice值,若出错,返回-1
int setpriority(int which, id_t who, int prio);
//返回值:若成功,返回0,若出错,返回-1


which可以候选三个值:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。

which控制这who参数是如何解释。如果who为0,则标示调用进程,进程组或者用户。如果which参数作用于多个进程,则返回所有进程中优先级最高的(最小nice值)。

prio增加到NZERO(系统默认的nice值)上,变为新的nice值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  磁盘 内核 应用