系列文章:
操作系统详解(1)——操作系统的作用
操作系统详解(2)——异常处理(Exception)
操作系统详解(3)——进程、并发和并行
简称PID(process id)
每个进程都有唯一的正数PID
getpid函数: 返回调用进程的PID
getppid: 返回父进程的PID
#include
#include
pid_t getpid(void);
pid_t getppid(void);
getpid 和 getppid 函数返回一个类型为 pid_t 的整数值,在 Linux 系统上它在 types.h 中被定义为 int
#include
void exit(int status);
exit 函数以 status 退出状态来终止进程
(另一种设置退出状态的方法是从主程序中 返回一个整数值)
**Tip:**fork是Unix操作系统独有的, 也就是linux和maxOS都有该函数,而Windows没有…由于《深入了解计算机系统》一书以linux系统为基础,所以想要测试代码的友友需要安装虚拟机(比如说VMWare或者使用Windows下子系统WSL). 当然, Windows下也可以用其它的方法实现类似的效果,只是比较麻烦.
父进程通过调用fork函数创建一个新的运行的子进程.
#include
#include
pid_t fork(void);
// 返回:子进程返回0,父进程返回子进程的PID,如果出错,则为-1。
新创建的子进程几乎但不完全与父进程相同。
共同点:
子进程得到与父进程(用户级)虚拟地址空间相同(但是独立的)一份副本
包括: 代码和数据段, 堆, 共享库, 用户栈
子进程还将获得与父进程打开文件描述符相同的副本
子进程可以读写父进程中打开的任何文件
不同点:
子进程与父进程PID不同
返回值的特性使得程序可以知道是在父进程还是子进程中执行.
下面是一个例子:
int main()
{
pid_t pid;
int x = 1;
pid = Fork();
if(pid == 0){ /* Child */
printf("child: x=%d\n", ++x);
exit(0);
}
/* Parent */
printf("parent: x=%d\n", --x);
exit(0);
}
这里的Fork是上一章提到的包装函数, 定义如下:
pid_t Fork(void)
{
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
void unix_error(char *msg) /* unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
当在Unix系统上运行这个程序时,我们得到下面的结果:
linux> ./fork
parent: x=0
child : x=2
在我们的系统上运行这个程序时,父进程先完成它的 printf语句,然后是子进程。然而,在另一个系统上可能正好相反。
父进程与子进程是并发执行的独立进程,内核能够以任意方式交替执行它们控制流中的指令.
因此不能假定父进程与子进程的执行顺序
该地址空间指的是虚拟地址.
也就是说,在这个例子中, 当fork返回时, 子进程和父进程中的x的值都是1. (相同)
然而, 父进程和子进程对x所做的任何改变都是独立的,不会反映在另一个进程的内存中。(独立)
在本例中, 体现为printf输出的内容不同.
在这个例子中, stdout(标准输出)就是子进程与父进程共享的文件,所以子进程与父进程的内容才会都输出到(同一个)屏幕上.
子进程会继承父进程所有的打开文件.
下面这个例子展示了如何用进程图理解fork的执行顺序:
#include
#include
#include
#include
int x = 10;
int main(){
int y = 1, i;
for(i - 0; i < 2; ++i){
pid_t pid = fork();
if(!pid){
x += y;
printf("x in child: %d\n", x);
exit(0);
}
y++;
x <<= 1;
printf("x in parent: %d\n", x);
return 0;
}
}
对进程图中所有顶点进行拓扑排序,即得到程序所有可能的输出结果.
(图中括号(x,y)表示输出前x与y的值)
比方说,一个可能的输出是:
x in parent: 20
x in parent: 40
x in child: 11
x in child: 22
所谓的拓扑排序,可以简单理解为,流程图中子分支上的所有事件一定在主分支上的事件之后发生.
在这个例子中,对x的值进行讨论:
(x=) 20 一定在40之前,也一定在22之前
如果更复杂一些,把exit(0)删去,程序的进程图如下:
(图中C表示输出"x in child"时x与y的值, P表示输出"x in parent"时x与y的值)
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。
不立即清除的原因是,子进程可能需要对父进程报告与其终止的相关信息(退出状态),可以看作是它的"遗言"
当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃子进程(不存在)
一个终止了但还未被回收的进程称为僵死进程(zombie)
如果子进程还未被回收,父进程已经被终止了, 内核会安排init进程回收它.
init进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。
Attention: 长时间运行的程序,比如shell或者服务器, 必须手动地去回收僵死子进程, 否则这些僵死进程仍会消耗内存资源.
那么如何手动回收子进程呢?
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
#include
#include
pid_t waitpid(pid_t pid, int *statusp, int options*);
// 返回: 成功,则为子进程的PID;
// WHOHANG, 则为0;
// 其他错误,则为-1
下面具体地解释它的3个参数:
等待集合的成员是由参数pid来确定的:
可以通过将options 设置为常量 WNOHANG, WUNTRACED和 WCONTINUED 的各种组合来修改默认行为:
可以用或运算把这些选项组合起来。例如:
如果 statusp参数非空,那么waitpid就会在 status 中放上关于导致返回的 子进程的状态信息(statusp->status),wait.h 头文件中定义了解释status参数的宏:
wait 函数是 waitpid 函数的简单版本:
#include
#include
pid_t wait(int *statusp);
调用 wait(&status) 等价于调用 waitpid(-1, &status, 0).
即默认对所有子进程挂起等待.
#include
#include
#include
#include
#include
void unix_error(char *msg) /* unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
#define N 2
int main()
{
int status, i;
pid_t pid;
/* Parent creates N children */
for (i = 0; i < N; i++)
if ((pid = Fork()) == 0) /* child */
exit(100+i);
/* Parent reaps N chds. in no particular order */
while((pid = waitpid(-1, &status, 0)) > 0){
if(WIFEXITED(status))
printf(("child %d terminated normally with exit status=%d\n", pid, WEXITSTATUS(status));
else
printf(("child %d terminated abnormally\n", pid);
}
/* The only normal term. is if there no more chds. */
/* 当回收了所有的子进程之后,再调用 waitpid 就返回-1,并且设置ECHILD */
if(errno != ECHILD) // 检查waitpid正常终止
unix_error("waitpid error");
exit(0);
}
一个示例输出:
unix>./waitpid1
child 22966 terminated normally with exit status=100
child 22967 terminated normally with exit status=101
但是由于子进程是并发的,两个子进程回收的顺序是不确定的.
对程序稍作修改, 消除了这种不确定性,按照父进程创建子进程的相同顺序来回收这些子进程.
int main()
{
int status, i;
pid_t pid[N], retpid;
/* Parent creates N children */
for (i = 0; i < N; i++)
if ((pid = Fork()) == 0) /* child */
exit(100+i);
/* Parent reaps N chds. in order */
i = 0;
while((retpid = waitpid(pid[i++], &status, 0)) > 0){
if(WIFEXITED(status))
printf(("child %d terminated normally with exit status=%d\n", retpid, WEXITSTATUS(status));
else
printf(("child %d terminated abnormally\n", retpid);
}
/* The only normal term. is if there no more chds. */
if(errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
#include
unsigned int sleep(unsigned int secs);
//Returns: seconds left to sleep
//如果已经睡满, 返回0
//如果休眠被信号中断而过早返回,返回还剩下的要休眠的秒数.
int pause(void);
//Always returns -1
//使该进程休眠直到收到信号
execve函数在当前进程的上下文中加载并运行一个新程序。
#include
int execve(const char *filename, const char *argv[], const char *envp[]);
//filename: 可执行文件名
//argv: argument list (命令行参数)
//envp: environment variable list (环境变量)
//does not return if OK, returns -1 on error
//正常情况下绝不返回
argv[0]
是可执行目标文件的名字。在execve加载了filename之后,它调用启动代码, 设置好栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型
int main(int argc, char **argv, char **envp);
或 int main(int argc, char *argv[], char *envp[]);
三个参数:
当main开始执行时, 用户栈的组织结构如下所示:
比如shell里面键入命令ls -lt /usr/include
, 就会运行一个新的程序 ls
并把ls -lt /usr/include
作为命令行参数传入, 会影响程序的执行方式.
#include
char *getenv(const char *name);
//Returns: ptr to name if exists, NULL if no match.
int setenv(const char *name, const char *newvalue, int overwrite);
//若overwrite非零, 用newvalue代替oldvalue
//Returns: 0 on success, -1 on error.
void unsetenv(const char *name);
//如果环境数组包含一个形如"name=oldvalue"的字符串,那么unsetenv会删除它
//Returns: nothing.
程序是一堆代码和数据;程序可以作为目标文件存在于磁盘上,或者作为段存在于地址空间中。
进程是执行中程序的一个具体的实例;程序总是运行在某个进程的上下文中。
fork在新的子进程中运行相同的程序, 新的子进程是父进程的一个复制品.
execve在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间, 但没有创建一个新进程. 新的程序仍然有相同的PID, 并且继承调用 execve 函数时已打开的所有文件描述符.
介绍了进程控制的相关函数, 包括fork, waitpid, sleep, execve, 其中fork与wait是极其重要且难懂的.
下一章将讲解信号机制以及实现.