第三章读书笔记
第三章读数笔记
Unix/Linux进程管理
3.1多任务管理
- 一般来说,多任务处理指的是同时进行几项独立活动的能力。在计算机技术中,多任务处理指的是同时执行几个独立的任务。在单处理器(单CPU)中,一次只能执行一个任务,多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行操作从一个任务切换到另一个任务。这种逻辑并行性被称为“并发”。在多处理器(多核CPU)中,同时执行多个任务这种并行性称为“并行”。
3.2进程的概念
- 在操作系统种中,任务也称为进程,与程序不同,程序是静态的,而进程是动态的。在实际应用中,进程和任务这两个术语可以互换。
- 进程的定义:进程是对映像的执行。
- 进程的数据结构:进程控制块(PCB)或任务控制块(TCB),称为PROC结构体。一个简单的PROC结构体如下:
- 单CPU系统中,操作系统内核经常会使用PROC指针,指向当前正在执行的PROC。
- 多CPU的多处理操作系统中,可在不同的CPU上实时、并行执行多个进程。因此多处理器系统中正在运行的[NCPU]可能是一个指针数组,每一个元素指向一个正在特定CPU上运行进程。
3.3多任务处理系统
多任务处理系统MT编程示例如下(示例代码过长,在此不予展示):
- type.h文件:定义了系统常数和表示进程的简单PROC结构体。
- ts.s文件:在32位GCC汇编代码中可实现进程上下文切换。
- queue.c文件:可实现队列和链表操作函数。
- t.c文件:t.c文件定义MT系统数据结构、系统初始化代码和进程管理函数。
3.4进程同步
- “并发”执行时协调各进程正确有序地执行。
- 最简单的进程同步工具:休眠与唤醒操作。
3.4.1休眠操作
- 休眠条件:当某个进程申请资源或等待事件发生却没能得到“满足”时,该进程就会在某个事件值上进入休眠状态,,该事件值表示休眠原因。
- 休眠操作实现:可在PROC结构体中添加一个event字段,并实现ksleep(int event)函数(当event参数满足休眠条件时,执行休眠操作)。
- 修改后的PROC结构体如下:
- ksleep(int event)函数算法如下:
int kwait(int *status)
{
if(caller has no child)return -1 for error;
while(1){//caller has children
search for a (any) ZOMBIE child;
if(found a ZOMBIE child){
get ZOMBIE child pid
copy ZOMBIE child exitCode to *status;
burry the ZUMBIE child(put its PROC back to freeList)
return ZOMBIE child pid;
}
ksleep(running);
}
}
###3.4.2唤醒操作
- 当某一个等待的时刻到来时,kwakeup()函数将被调用以唤醒等待这一时间值的所有程序,值得一提的是被唤醒的进程不会被允许立刻申请资源,而是一个一个排队等待到来的资源。
- kwakeup()函数算法如下:
3.5进程终止
- 正常终止:进程调用exit(value)。发出-exit(value)系统调用来执行在操作系统内核中的kexit(value)。
- 异常终止:进程因为某个信号而异常终止。
以上二者均会调用kexit()函数。
2.5.3等待进程中止
任何时候,进程都可以调用内核函数
pid =kwait(int *status)
等待僵尸子进程。如果成功,则返回的pid是僵尸子进程的pid,而status包含僵尸子进程的退出代码。此外,kwait()还会将僵尸子进程释放回freeList以便重用。kwait的算法如下:
点击查看代码
int kwait(int *status)
{
if(caller has no child)return -1 for error;
while(1){//caller has children
search for a (any) ZOMBIE child;
if(found a ZOMBIE child){
get ZOMBIE child pid
copy ZOMBIE child exitCode to *status;
burry the ZUMBIE child(put its PROC back to freeList)
return ZOMBIE child pid;
}
ksleep(running);
}
}
3.7Unix/Linux中的进程
进程来源
- 操作系统启动时,内核会强制创建一个PID=0的初始进程,通过分配PROC结构体(PROC[0])进行创建,初始化PROC内容,并让运行指向proc[0];系统执行初始化进程P0;大多数操作系统都以这种方式开始第一个进程。P0继续初始化系统,包括系统硬件和内核数据结构。挂载一个根文件系统,使系统可以使用文件。初始化系统后,P0复刻出一个子进程P1,并把进程切换为用户模式运行P1。
INIT和守护进程
- P1通常被称为INIT进程,因为他的执行映像是init程序。
- 守护进程:在后台运行,不与任何用户交互
登录进程
- 每个LOGIN进程打开三个与自己的终端相关联的文件流,这三个文件流分别是用于标准输入的stdin、标准输出的stdout、用于标准错误消息的stderr。每个文件流都是指向进程堆栈区中FILE结构体的指针。
sh进程
- 用户登录成功时,LOGIN进程会获取用户的gid和uid,从而成为用户的进程。它将目录更改为用户的主目录并执行列出的程序,通常是命令解释程序sh(通常称为sh进程)。它提示用户执行命令。一些特殊的命令,如成cd(更改目录)、退出、注销等,由sh自己直接执行。其他大多数命令是各种bin目录中的可执行文件。
对于每个(可执行文件)命令,sh会复刻一个子进程,并等待子进程终止。子进程将其执行映像更改为命令文件并执行命令程序。子进程在终止时会唤醒父进程sh,父进程会收集子进程终止状态、释放子进程PROC结构体并提示执行另一个命令等。除简单命令外,sh还支持I/O重定向和通过管道连接的多个命令。
进程的执行模式
- Unix/Linux中,进程以两种不同的模式执行,即内核模式和用户模式,简称Kmode和Umode。
- 在进程的生命周期中,会在Kmode和Umode之间发生多次迁移。每个进程都在Kmode下产生并开始执行。
- 在Kmode下执行所有相关操作,包括终止。在Kmode下通过将CPU状态寄存器从K模式更改为U模式,可以轻松切换到Umode。但是进入Umode就不能够随意更改CPU状态了。Umode进程只能通过下面三种方式进入Kmode:
(1)中断:外部设备发送给CPU信号,请求CPU服务。当在Umode下执行时,CPU中断是启用的,因此它将响应任何中断。中断发生时,CPU将进入Kmode处理中断,这将导致进程进入Kmode;
(2)陷阱:陷阱是错误条件,错误条件被CPU识别为异常,使得CPU进入Kmode来处理错误。在Unix/Linux中内核陷阱处理程序将陷阱原因转换为信号编号,并将信号传递给进程。对于大多数信号,进程的默认操作是终止。
(3)系统调用(syscall):允许Umode进程进入Kmode以执行内核函数的机制。当某进程执行完内核函数后,它将期望结果和一个返回值返回到Umode,0表示成功,1表示错误。发生错误,外部全局变量errno(在errno.h中)会包含一个ERROR代码,用于标识错误。
3.8进程中的系统调用
- fork()函数
Usage: int pid = fork();
fork()创建子进程并返回该子进程的Pid
fork()函数可能有三种不同的返回值: 1.在父进程中,fork返回新创建子进程的进程ID; 2.在子进程中,fork返回0; 3.如果出现错误,fork返回一个负值; - sleep()函数:是进程进入休眠状态
- exit()函数:执行一些清理工作,如刷新stdout,关闭I/O流等,然后终止进程;也可以先不执行而直接关闭进程
- wait()函数:系统调用,,等待僵尸子进程,若成功则返回其Pid;还可以释放僵尸子进程以供重新使用
- exec()函数:更改进程执行映像
3.9I/O重定向
3.9.1文件流和文件描述符
- sh进程有三个用于终端I/O的文件流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误)。其文件描述符分别对应0、1、2。其结构体如下:
3.10管道
-
管道是用于进程交换数据的单向进程通信通道。管道有一个读取端和一个写入端。可以从管道的读取端读取写入端的数据。目前有普通管道和双向管道两种,双向管道是数据可以双向传输的管道。
-
命令管道又叫FIFO:
(1)在sh中,通过mknod命令创建一个命令管道:
(2)或在c语言中发出mknod()系统调用
(3)进程可像访问普通文件一样发个文命名管道。
实践内容
fork()函数示例:
- 代码如下:
点击查看代码
#include <unistd.h>
#include <stdio.h>
int main ()
{
int pid;
printf("this is %d my parent=%d\n",getpid(),getppid());
pid = fork();
if (pid)
{
printf("i am the process %d, child=%d\n", getpid(),pid);
}
else
{
printf("i am the process %d, parent=%d\n", getpid(),getppid());
}
return 0;
}
“CHILD PID=d\n” 应为 “CHILD PID=%d\n”
示例3.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.取消注释sleep(1) 结果
-
2.取消注释sleep(2) 结果
-
3.取消注释sleep(1)和sleep(2) 结果
- 可知2与3的打印间隔时间是一样的,与书中所述情况一致。
示例3.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);
}
}
-实践结果:
- 子程序终止状态为0X6400,与书中一致。
示例3.4实践
- 书中代码如下:
点击查看代码
#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());
}
}
}
- 实践结果如下: