第3章 Unix/Linux进程管理
1 知识点归纳
1.1 多任务处理
一般来说,多任务处理指的是同时进行几项独活动的能力。在计算机技术中,多任务处理指的是同时执行几个独立的任务。多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行任务从一个任务切换到另一个任务。
1.2 进程的概念
进程的正式定义:进程是对映像的执行。
一个简单的PROC结构体:
typedef struct proc{
struct proc *next; // next proc pointer
int *ksp; // saved sp: at byte offset 4
int pid; // process ID
int ppid; // parent process pid
int status; // PROC status=FREE|READY, etc.
int priority; // scheduling priority
int kstack[1024]; // process execution stack
}PROC;
在PROC结构体中:
- next是指向下一个PROC结构体的指针,用于在各种动态数据结构(如链表和队列)中维护PROC结构体。ksp字段是保存的堆栈指针。
- pid是标识一个进程的ID编号,
- ppid是父进程ID的编号,
- status是进程的当前状态,
- priority是进程调度优先级,
- kstack是进程执行的堆栈。
1.3 Unix/Linux中的进程
1.3.1 进程来源
操作系统启动时,内核会强制创建一个PID=0的初始进程,即通过分配PROC结构体(通常是PROC[0])进行创建,初始化PROC内容,并让运行指向proc[0];系统执行初始化进程P0;大多数操作系统都以这种方式开始第一个进程。P0继续初始化系统,包括系统硬件和内核数据结构。然后,它挂载一个根文件系统,使系统可以使用文件。初始化系统后,P0复刻出一个子进程P1,并把进程切换为以用户模式运行P1。
1.3.2 INIT和守护进程
当进程P1开始运行时,它将其执行镜像改为INIT程序。因此,P1通常被称为INIT进程,因为它的执行映像是init程序。P1开始复刻出许多子进程。P1的大部分子进程都是用来提供系统服务的。它们在后台运行,不与任何用户交互。这样的进程称为守护进程。守护进程的例子有:
syslogd :log daemon process
inetd :Internet service daemon process
httpd :HTTP server daemon process
etc.
1.3.3 登录进程
除了守护进程之外,P1还复刻了许多LOGIN进程,每个终端上一个,用于用户登录。每个LOGIN进程打开三个与自己的终端相关联的文件流,这三个文件流是用于标准输入的stdin、用于标准输出的stdout和用于标准错误消息的stderr。每个文件流都是指向进程堆区中FILE结构体的指针。每个FILE结构体记录一个一个文件描述符(数字),stdin的文件描述符是0,stdout的是1,stderr的是2。然后,每个LOGIN进程向stdout显示一个
login:
以等待用户登录。
1.3.4 sh进程
当用户成功登录时,LOGIN进程会获取用户的gid和uid,从而成为用户的进程。它将目录更改为用户的主目录并列执行出的程序,通常是命令解释程序sh。现在,用户进程执行sh,因此用户进程通常称为sh进程。
1.3.5 进程的执行模式
Unix/Linux中,进程以两种不同的模式执行,即内核模式和用户模式,简称Kmode和Umode。如图3.4所示。
在进程的生命周期中,会在Kmode和Umode之间发生多次迁移。每个进程都在Kmode下产生并开始执行。事实上,它在Kmode下执行所有相关操作,包括终止。在Kmode下通过将CPU状态寄存器从K模式更改为U模式,可以轻松切换到Umode。但是进入Umode就不能够随意更改CPU状态了。Umode进程只能通过下面三种方式进入Kmode:
- 中断:外部设备发送给CPU信号,请求CPU服务。当在Umode下执行时,CPU中断是启用的,因此它将响应任何中断。中断发生时,CPU将进入Kmode处理中断,这将导致进程进入Kmode;
- 陷阱:陷阱是错误条件,错误条件被CPU识别为异常,使得CPU进入Kmode来处理错误。在Unix/Linux中内核陷阱处理程序将陷阱原因转换为信号编号,并将信号传递给进程。对于大多数信号,进程的默认操作是终止。
- 系统调用(syscall):允许Umode进程进入Kmode以执行内核函数的机制。当某进程执行完内核函数后,它将期望结果和一个返回值返回到Umode,0表示成功,1表示错误。发生错误,外部全局变量errno(在errno.h中)会包含一个ERROR代码,用于标识错误。
1.4 进程管理的系统调用
在本节中,我们将讨论Linux中与进程管理相关的以下系统调用:
fork(),wait(),exec(),exit()
每个都是发出时间系统调用的库函数:
int syscall(int a,int b,int c,int d);
其中,第一个参数a表示系统调用号,b、c、d表示对应核函数的参数。
1.4.1 fork()函数
Usage: int pid = fork();
fork()创建子进程并返回该子进程的Pid,如果fork()失败则返回-1。
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid;
printf("this is %d my parent = %d\n", getpid(),getppid());
fpid = fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0)
{
printf("i am the child process, my process id is %d/n", getpid());//返回调用进程的PID
}
else
{
printf("i am the parent process, my process id is %d/n", getpid());//返回调用进程的PID
}
return 0;
}
1.4.2 进程执行顺序
在fork()完成后,子进程与父进程和系统中所有其他进程竞争CPU运行时间。接下来运行哪个进程取决于它们的调度优先级,优先级呈动态变化。
下面的示例演示了进程可能的各种执行顺序。
#include <stdio.h>
int main()
{
int pid=fork(); // fork a child
if (pid)
{
printf("PARENT %d CHILD=%d\n", getpid(), pid);
//sleep(1);
printf("PARENT %d EXIT\n", getpid());
}
else
{
printf("child %d start my parent«%d\n", getpid(), getppid());
// sleep(2); // sleep 2 seconds -> let parent die first
printf("child %d exit my parent=%d\n", getpid(), getppid());
}
}
1.4.3 等待子程序终止
在任何一个时候,一个进程都可以使用
int pid = wait(int *status);
系统调用,等待僵尸子进程。如果成功,则wait()会返回僵尸子进程的PID,而且status包含僵尸子进程的的exitCode()。此外,wait()还会释放僵尸子进程,以供重新使用。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int pid, status;
pid = fork();
if (pid)
{
printf("PARENT %d WAITS FOR CHILD %d TO DIE\n", getpid(),pid);
pid = wait(&status); // wait for ZOMBIE child process
printf("DEAD CHILD=%d, status=0x%04x\n", pid, status);
} // PARENT:
else
{
printf("child %d dies by exit(VALUE)\n", getpid());
exit(100);
}
}
1.4.4 Linux中的subreaper进程
自内核3.4版本以来,Linux处理孤儿进程的方式略有不同。进程可以用系统调用将自己定义为subreaper进程:
prctl(PR_SET_SUBREAPER);
这样,init进程P1将不再是孤儿进程的父进程。
下面程序演示了Linux中的sub'reaper进程。
#include <stdio.h>
#include <unistd.h>
#include <wait.h>
#include <sys/prctl.h>
int main()
{
int pid,r,status;
printf("mark process %d as a subreaper\n",getpid());
r = prctl(PR_SET_CHILD_SUBREAPER);
pid = fork();
if(pid)
{
printf("subreaper %d child = %d\n", getpid(), pid);
while (1)
{
pid = wait(&status);
if (pid > 0)
{
printf("subreaper %d waited a ZOMBIE=%d\n",getpid(), pid);}
else
break;
}
}
else
{
printf("child %d parent = %d\n", getpid(), (pid_t)getppid);
pid = fork();
if (pid)
{
printf("child=%d start: grandchild=%d\n", getpid(),pid);
printf("child=%d EXIT: grandchild=%d\n",getpid(),pid);
}
else
{
printf("grandchild=%d start:myparent=%d\n",getpid(),getppid());
printf("grandcild=%d EXIT:myparent=%d\n", getpid(),getppid());
}
}
}
2 实践内容与截图
2.1 fork()实践
代码:
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid;
printf("this is %d my parent = %d\n", getpid(),getppid());
fpid = fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0)
{
printf("i am the child process, my process id is %d/n", getpid());//返回调用进程的PID
}
else
{
printf("i am the parent process, my process id is %d/n", getpid());//返回调用进程的PID
}
return 0;
}
2.2 进程执行顺序演示
代码:
#include <stdio.h>
int main()
{
int pid=fork(); // fork a child
if (pid)
{
printf("PARENT %d CHILD=%d\n", getpid(), pid);
//sleep(1);
printf("PARENT %d EXIT\n", getpid());
}
else
{
printf("child %d start my parent«%d\n", getpid(), getppid());
// sleep(2); // sleep 2 seconds -> let parent die first
printf("child %d exit my parent=%d\n", getpid(), getppid());
}
}
取消第(1)行注释,子进程先运行完成:
只取消第(2)行注释,子进程的ppiid改为其他PID号
2.3 等待和退出系统调用
#include <stdio.h>
#include <stdlib.h>
int main()
{
int pid, status;
pid = fork();
if (pid)
{
printf("PARENT %d WAITS FOR CHILD %d TO DIE\n", getpid(),pid);
pid = wait(&status); // wait for ZOMBIE child process
printf("DEAD CHILD=%d, status=0x%04x\n", pid, status);
} // PARENT:
else
{
printf("child %d dies by exit(VALUE)\n", getpid());
exit(100);
}
}
2.4 Linux中的subreaper进程
#include <stdio.h>
#include <unistd.h>
#include <wait.h>
#include <sys/prctl.h>
int main()
{
int pid,r,status;
printf("mark process %d as a subreaper\n",getpid());
r = prctl(PR_SET_CHILD_SUBREAPER);
pid = fork();
if(pid)
{
printf("subreaper %d child = %d\n", getpid(), pid);
while (1)
{
pid = wait(&status);
if (pid > 0)
{
printf("subreaper %d waited a ZOMBIE=%d\n",getpid(), pid);}
else
break;
}
}
else
{
printf("child %d parent = %d\n", getpid(), (pid_t)getppid);
pid = fork();
if (pid)
{
printf("child=%d start: grandchild=%d\n", getpid(),pid);
printf("child=%d EXIT: grandchild=%d\n",getpid(),pid);
}
else
{
printf("grandchild=%d start:myparent=%d\n",getpid(),getppid());
printf("grandcild=%d EXIT:myparent=%d\n", getpid(),getppid());
}
}
}
3 总结
本章主要讨论了UNIX/LINUX进程管理,阐述了多任务处理原则,并使用实践了用于进程管理的系统调用,包括fork、wait、exec和exit;也实践了父进程与子进程的关系,包括进程终止和父进程等待操作之间关系的详细描述。