这一期的内容比较轻松,主要是搞懂 exit.c 的代码,其中涉及到有关内存管理的函数也不深入,等到了 mm 模块再做分析,一些函数如果没有给出定义,那就是现在只需要知道它们的功能就好了。另外,下一篇文章将会阅读 fork.c,结合 sched.c,exit.c,fork.c 再回味一遍,就能构建起一个进程创建、调度、终止的框架


exit.c 主要实现了终止进程与进程退出的相关函数,即系统调用 exit 的处理流程。当调用 exit 函数时(参数为用户程序提供的退出码),实际调用 sys_exit。sys_exit 又调用 do_exit ,并保留退出码低字节,左移 8 位后当作 do_exit 的参数,空出来的这 8 位将用于保存 wait 函数的状态信息

1
2
3
4
5
// Line 137
int sys_exit(int error_code)
{
return do_exit((error_code&0xff)<<8);
}

do_exit 函数根据当前进程的特性对其进行处理,并把当前进程状态设置为僵死态,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Line 102
int do_exit(long code)
{
int i;
// 释放当前进程 代码段 和 数据段 使用的内存页
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
for (i=0 ; i<NR_TASKS ; i++) // 遍历任务进程数组
if (task[i] && task[i]->father == current->pid) { // 如果该进程是当前进程的子进程
task[i]->father = 1; // 则将该进程的父进程设置为 1 号进程
if (task[i]->state == TASK_ZOMBIE) // 如果该进程是僵死状态
(void) send_sig(SIGCHLD, task[1], 1); // 则向 1 号进程发送 SIGHCLD 信号
}
for (i=0 ; i<NR_OPEN ; i++) // 将当前进程打开的所有文件关闭
if (current->filp[i])
sys_close(i);
iput(current->pwd); // 对当前进程的工作目录(pwd),根目录(root),
current->pwd=NULL; // 执行程序文件(executable)的 i 节点进行同步,
iput(current->root); // 放回各个 i 节点并置空(释放)
current->root=NULL;
iput(current->executable);
current->executable=NULL;
// 当前进程如果是会话的领头进程(leader),并且有控制终端
if (current->leader && current->tty >= 0)
tty_table[current->tty].pgrp = 0; // 则释放该终端
if (last_task_used_math == current) // 如果当前进程上次使用了协处理器
last_task_used_math = NULL; // 将记录的信息置空
if (current->leader) // 当前进程如果是会话的领头进程
kill_session(); // 则将该会话关闭
current->state = TASK_ZOMBIE; // 将当前进程状态置为僵死态
current->exit_code = code; // 当前进程退出码设置为用户程序给定的退出码
tell_father(current->father); // 向当前进程的父进程发送 SIGCHLD 信号,并释放内存
schedule(); // 调用进程调度函数
return (-1); // 不会执行到这里来,加这一句为了防止编译的时候报 warning
}

然后是 do_exit 中调用的一些函数的实现(按从上到下的顺序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Line 35
// 参数 sig 是信号值,p 目标任务的结构体指针,priv 控制是否强制发送信号
static inline int send_sig(long sig,struct task_struct * p,int priv)
{
if (!p || sig<1 || sig>32) // 判断 p 非空且信号值在 1 ~ 32 之间
return -EINVAL;
// 如果强制发送信号,或者当前进程有效用户 ID 与目标进程相同,或者是超级用户
if (priv || (current->euid==p->euid) || suser())
p->signal |= (1<<(sig-1)); // 则向目标任务发送该信号
else
return -EPERM;
return 0;
}

// Line 46
static void kill_session(void)
{
struct task_struct **p = NR_TASKS + task;

while (--p > &FIRST_TASK) { // 从任务数组最后一个任务往前遍历
if (*p && (*p)->session == current->session) // 如果该数组项非空且属于当前任务会话组
(*p)->signal |= 1<<(SIGHUP-1); // 则向其发送 SIGHUP 挂断信号
}
}

// Line 83
// 参数 pid 是父进程的 pid
static void tell_father(int pid)
{
int i;

if (pid)
for (i=0;i<NR_TASKS;i++) { // 遍历数组
if (!task[i]) // 跳过空项
continue;
if (task[i]->pid != pid) // 跳过 pid 不相等的项,直到找到父进程
continue;
task[i]->signal |= (1<<(SIGCHLD-1)); // 向其发送 SIGCHLD 信号
return;
}
printk("BAD BAD - no father found\n\r"); // 来到这里就是没找到对应进程
release(current); // 则释放当前进程占用的内存
}

// Line 19
void release(struct task_struct * p)
{
int i;

if (!p) // 任务结构体指针需为非空
return;
for (i=1 ; i<NR_TASKS ; i++) // 从 1 号进程的任务结构体开始遍历任务数组
if (task[i]==p) { // 如果该项就是要释放的任务
task[i]=NULL; // 则将任务结构体数组中该项置空(释放)
free_page((long)p); // 释放任务占用的内存页面
schedule(); // 调用调度函数
return;
}
panic("trying to release non-existent task");
}

exit.c 中还剩下两个函数,sys_kill 是系统调用 kill 的处理函数,用于向给定的 pid 对应的进程发送一个信号;sys_waitpid 是系统调用 waitpid 的处理函数,作用是挂起当前进程直到 pid 指定的子程序终止等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Line 60
int sys_kill(int pid,int sig)
{
struct task_struct **p = NR_TASKS + task; // 从任务数组最后一项开始遍历
int err, retval = 0;

if (!pid) while (--p > &FIRST_TASK) { // 如果 pid 为 0
if (*p && (*p)->pgrp == current->pid) // 向当前进程组中所有进程发送信号
if (err=send_sig(sig,*p,1))
retval = err;
} else if (pid>0) while (--p > &FIRST_TASK) { // 如果 pid > 0
if (*p && (*p)->pid == pid) // 只将信号发送给进程号是 pid 的进程
if (err=send_sig(sig,*p,0))
retval = err;
} else if (pid == -1) while (--p > &FIRST_TASK) // 如果 pid 为 -1
if (err = send_sig(sig,*p,0)) // 将信号发送给除了初始进程之外的所有进程
retval = err;
else while (--p > &FIRST_TASK) // 如果 pid < -1
if (*p && (*p)->pgrp == -pid) // 将信号发送给进程组 -pid 的所有进程
if (err = send_sig(sig,*p,0))
retval = err;
return retval;
}

// Line 142
// 参数 pid 是进程号,stat_addr 是保存状态信息位置的指针,options 是选项
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
int flag, code; // flag 标志用于表示选出的子进程处于 睡眠/就绪 态
struct task_struct ** p;

verify_area(stat_addr,4); // 验证用于存放状态信息的位置内存空间足够
repeat:
flag=0;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) { // 从任务数组最后一项开始遍历
if (!*p || *p == current) // 如果该项为空或该项就是当前进程,则跳过
continue;
if ((*p)->father != current->pid) // 如果该项的父进程不是当前任务,则跳过
continue;
if (pid>0) { // 如果参数 pid > 0
if ((*p)->pid != pid) // 如果是当前进程的其他子进程,则跳过
continue;
} else if (!pid) { // 如果参数 pid 为 0,表示等待与当前进程组号相同的任何子进程
if ((*p)->pgrp != current->pgrp) // 如果当前项和当前进程不在同一进程组,则跳过
continue;
} else if (pid != -1) { // 如果参数 pid < -1,表示等待进程组号为 -pid 的任何子进程
if ((*p)->pgrp != -pid) // 如果当前项不在 -pid 的进程组,则跳过
continue;
}
// 如果参数 pid 为 -1,会直接来到这里,表示当前进程等待其任意子进程
switch ((*p)->state) { // 判断选出来的子进程状态
case TASK_STOPPED: // 如果是停止状态
if (!(options & WUNTRACED)) // 且参数 options 中 WUNTRACED 置位
continue; // WUNTRACED 表示返回终止子进程信息和因信号停止的子进程信息
put_fs_long(0x7f,stat_addr); // 则将退出时的状态 0x7f 放到 stat_addr 处
return (*p)->pid; // 返回子进程 pid
case TASK_ZOMBIE: // 如果是僵死状态
// 将其在内核态与用户态运行的时间分别加在当前进程(父进程)对应字段中
current->cutime += (*p)->utime;
current->cstime += (*p)->stime;
flag = (*p)->pid; // 子进程 pid 保存在 flag 中
code = (*p)->exit_code; // 子进程退出码保存在 code 中
release(*p); // 释放子进程
put_fs_long(code,stat_addr); // 退出码放到 stat_addr 处
return flag; // 返回子进程 pid
default: // 来到这里表示满足条件的子进程处于运行或睡眠态
flag=1;
continue;
}
}
if (flag) { // 如果满足条件的子进程处于运行或睡眠态
if (options & WNOHANG) // 如果参数 options 中 WNOHANG 置位,直接返回
return 0; // WNOHANG 表示若没有子进程处于退出或终止态就返回
current->state=TASK_INTERRUPTIBLE; // 否则将当前进程的状态置为可中断睡眠态
schedule(); // 调用进程调度函数
// 当系统又执行本进程时,若本进程没有收到除 SIGCHLD 外的信号,则返回到开头重复处理
if (!(current->signal &= ~(1<<(SIGCHLD-1))))
goto repeat;
else // 否则返回 -EINTR
return -EINTR;
}
// 如果 flag 为 0,表示子进程不存在,返回
return -ECHILD;
}

我们平常在程序中调用的 wait 函数定义在 lib/wait.c 中,wait 实际就是通过调用 sys_waitpid 实现的:

1
2
3
4
5
6
7
// lib/wait.c
_syscall3(pid_t,waitpid,pid_t,pid,int *,wait_stat,int,options) // 对应 sys_waitpid

pid_t wait(int * wait_stat)
{
return waitpid(-1,wait_stat,0);
}