Li Jiaheng's blog
1995 字
10 分钟
程序控制流(2) 进程控制
WARNING

这是《深入理解计算机系统》(CS)的笔记,彼时还未上CO,OS等计算机系统基础课,存在理解不到位与错误之处.

Unix提供了大量C程序中操作进程的系统调用,这里介绍其中几个重要的函数。为难的是这些函数对应的头文件比较混乱,并且很多在windows下不兼容。

系统调用错误处理#

在正式开始介绍进程操作的系统调用前,先看一看系统调用函数的错误处理与包装。系统调用中某个库定义了全局整数变量 errno,当Unix系统级函数出错时,它通常会返回-1。通过 errno 的值可以解码出定义过的几个错误类型,使用 strerror() 函数可以对这串数字解码,返回和它关联的错误字符串。(我暂不确定这是不是库函数)。
为了稳定,必须由错误处理成分,虽然系统调用出错的概率看起来很小。
比如,对常用的 fork() 函数的含错误处理的包装:

/* 未包装版本 */  
if((pid=fork())<0){    //fork()出错返回-1  
	fprintf(stderr,"fork error: %s\n",strerror(errno));  
	exit(0);    //终止此进程.  
}  
/* 含错误处理包装 */  
/* 首先是更普适的错误表达函数,unix风格 */  
void unix_error(char *msg)    //msg是在调用什么的时候出错  
{  
	fprintf(stderr,"%s: %s\n",msg,strerror(errno));  
	exit(0);  
}  
/* 包装的fork:Fork() */  
pid_t Fork(void)  
{  
	pid_t pid;  
	if((pid=fork())<0){  
		unix_error("fork error");  
	}  
	return pid;  
}  

通过这样简单方式包装的函数使得代码简化、可读性更高,用原函数首字母大写表示新的包装函数(比如fork—>Fork)。

获取进程ID (PID)#

每个进程都有一个唯一的识别ID (PID),用 getpid 返回本进程 pid,用 getppid 返回父进程 pid。Linux下每个进程都一定至少有父进程 _init。这和它运行程序的过程有关系。在《链接概述》中有稍微提及。

#include<sys/types.h>  
#include<unistd.h>  
pid_t getpid(void);  
pid_t getppid(void);  
//Linux下pid_t被定义为int。  

终止进程#

从程序员角度,进程只有3种状态:运行,停止(挂起),终止。后两者都可能是某种异常或者信号所致,也可以通过系统调用主动控制。用 exit() 函数主动终止进程。(另一种终止方法是从main程序中 return 一个整数值)。

#include<stdlib.h>  
void exit(int status);  

以status退出状态来终止进程。

创建子进程#

用 fork() 函数创建子进程,创建出来的子进程最开始其实是对父进程拷贝,有相同的上下文,共用同一个代码文件。从这个函数开始,进程控制函数都比较微妙。fork() 函数调用一次,返回两次。注意返回两次,是一次返回到原进程中相同位置,另一次返回到子进程中相同位置,注意先返回到那个进程中是不确定的!

#include<sys/types.h>  
#include<unistd.h>  
pid_t fork(void);  
//Windows下不支持。若出错返回-1  
//返回到原本进程的时候,返回值为所创建的子进程的pid。  
//返回到子进程的时候,返回值为0.  

通过返回值可以判断自己处于子进程还是父进程。

int main()  
{  
	pid_t pid;  
	int x=1;  
	pid=Fork();  
	if(pid==0){  //返回到子进程  
		printf("child: %d\n",++x);  
		exit(0);  
	}  
	printf("parent: %d\n",--x);  
	exit(0);  
}  

运行后,可能的结果为:

child: 2  
parent: 0  
或者  
parent: 0  
child: 2  

父子进程由相同的上下文环境,因而都有等值的全局变量x。但是父子进程是不同进程,对自己的x的改变不影响另一个进程。同样,子进程继承了父进程的文件打开状态,父进程打开了stdout,子进程也就打开了stdout,因而子进程的printf也输出到同一个屏幕上。
另外可以看到,fork先返回到父进程还是子进程时随机不可控的。
通过对返回值的判断,可以让程序知道是在子进程中还是父进程中。
当fork比较多的时候,可以画进程图 辅助思考。比如:

int main()  
{  
	Fork();  
	Fork();  
	printf("hello\n");  
	exit(0);  
}  

一共有四行hello。

回收子进程(P517)#

当子进程终止,内核并不立刻将其回收清理,而是保持为终止的状态,等父进程回收它。此过程中,这个终止的子进程被称为僵死进程(Zombie)
子进程实际上可以脱离父进程独立存在。当子进程未终止而父进程终止的时候,内核会安排 init 进程为它的养父,由 init 回收它。init在系统启动的时候创建,是所有进程的祖先,不会停止。长时间运行的进程总该试图回收它的子进程,僵死进程将消耗系统资源。
用waitpid()函数和它的简化wait() 函数回收子进程。

#include<sys/types.h>  
#include<sys/wait.h>  
pid_t waitpid(pid_t pid, int *statusp, int options);  
pid_t wait(int *statusp);  

waitpid 中,pid 参数是等待集合,options 是默认行为,statusp 用来检查退出状态
默认情况下(options=0),waitpid挂起调用它的进程,直到等它的等待集合中的一个子进程终止;如果调用的时候已经有子进程终止,则立刻返回。这种状况下,waitpid回收这个终止的子进程,并返回它的pid,将 statusp设置为该子进程的退出状态。
出错则返回-1.

1)等待集合pid
如果pid>0,则只等这个特定的子进程。
如果pid=-1,等待集合为该进程的所有子进程
waitpid还包括其它等待集合,比如进程组。

2)修改默认行为 options
options可以修改为常量 WNOHANG WUNTRACED WCONTINUE.
WNOHANG 试探一下,如果等待集合中没有子进程结束,则立即返回0,不等。
WUNTRACED 挂起调用进程,当等待集合中有成员终止或者停止,则返回。
WCONTINUE 等等待集合中有一个终止,或者有一个已经停止的成员收到 SIGCONT 信号重新开始执行,则返回。
可以对它们用或运算组合:
WNOHANG | WUNTRACED 没有子进程停止或终止的时候,立刻返回。

3)检查已回收子进程的退出状态。
通过 status(statusp 指向的值) 的值检查。wait.h 中定义了几个解释退出状态的宏函数:
WIFEXITED(status):如果通过exit 或return正常退出,这个宏函数返回1.
WEXITSTATUS(status):返回正常终止的子程序的退出状态。就是exit(int status)、return xx 中的那个值。必须先调用上一个宏函数。
WIFSIGNALED(status):是被一个未被捕获的信号终止的,就返回真。
WTERMSIG(status):返回导致停止的信号的编号。必须先调用上一个宏函数。
WIFSTOPPED(status):引起返回的子程序当前是停止(而非终止)的。
WSTOPSIG(status):引起停止的信号编号。
WIFCONTINUE(status):引起返回的子进程是收到SIGCONT信号继续执行。

4)错误条件:
出错返回-1。如果没有子进程,设置 errno 为ECHILD 。如果waitpid被信号中断,设置 errno 为 EINTR

wait(stautsp); == waitpid(-1, statusp, 0);

进程休眠#

#include<unistd.h>  
int pause(void);  //让函数休眠,直到收到一个信号  
unsigned int sleep(unsigned int secs);  //有可能中途被信号打断sleep。返回还剩下未sleep的时间。  

加载、运行程序#

使用execve()保留 当前进程的所有上下文,并且加载新程序并执行。

#include<unistd.h>  
int execve(const char *filename, const char *argv[], const char *envp[]);  

加载运行目标文件,并有命令行和环境变量。
主函数的完整定义是:

int main(int argc, char *argv[], char *envp[]);  

环境变量的格式为:条目name=值、字符串

有几个操作环境变量的函数:

#include<stdlib.h>  
char *getenv(const char *name);  //查找是否有条目name,若有,返回value  
int setenv(const char *name,const char *newvalue, int overwrite);  
//若已有name,当overwrite!=0,用newvalue覆盖掉。如果无,则加上。  
void unsetenv(const char *name);  //删除  

有了这些函数,已经可以写一个超级简化版的my_shell了。

程序控制流(2) 进程控制
https://namisntimpot.github.io/posts/computersystem/程序控制流/2进程控制/
作者
Li Jiaheng
发布于
2022-08-15
许可协议
CC BY-NC-SA 4.0