操作系统课程实验-linux-0-11基于内核栈切换的进程切换

实验四 基于内核栈的进程切换

一、linux-0.11进程切换过程

有两个重要的数据结构tss_structtask_struct

1. task_struct

也称为进程控制块PCB,一个进程所需的内容都被保存在这里

// 来自linux内核解析
// include/linux/sched.h
struct task_struct {
long state; // 任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)。
long counter; // 任务运行时间计数(递减)(滴答数),运行时间片。
long priority; // 优先数。任务开始运行时 counter=priority,越大运行越长。
long signal; // 信号位图,每个比特位代表一种信号,信号值=位偏移值+1。
struct sigaction sigaction[32]; // 信号执行属性结构,对应信号将要执行的操作和标志信息。
long blocked; // 进程信号屏蔽码(对应信号位图)。
int exit_code; // 任务停止执行后的退出码,其父进程会来取。
unsigned long start_code; // 代码段地址。
unsigned long end_code; // 代码长度(字节数)。
unsigned long end_data; // 代码长度 + 数据长度(字节数)。
unsigned long brk; // 总长度(字节数)。
unsigned long start_stack; // 堆栈段地址。
long pid; // 进程标识号(进程号)。
long pgrp; // 进程组号。
long session; // 会话号。
long leader; // 会话首领。
int groups[NGROUPS]; // 进程所属组号。一个进程可属于多个组。
task_struct *p_pptr; // 指向父进程的指针。
task_struct *p_cptr; // 指向最新子进程的指针。
task_struct *p_ysptr; // 指向比自己后创建的相邻进程的指针。
task_struct *p_osptr; // 指向比自己早创建的相邻进程的指针。
unsigned short uid; // 用户标识号(用户 id)。
unsigned short euid; // 有效用户 id。
unsigned short suid; // 保存的用户 id。
unsigned short gid; // 组标识号(组 id)。
unsigned short egid; // 有效组 id。
unsigned short sgid; // 保存的组 id。
long timeout; // 内核定时器超时值。
long alarm; // 报警定时值(滴答数)。
long utime; // 用户态运行时间(滴答数)。
long stime; // 系统态运行时间(滴答数)。
long cutime; // 子进程用户态运行时间。
long cstime; // 子进程系统态运行时间。
long start_time; // 进程开始运行时刻。
struct rlimit rlim[RLIM_NLIMITS]; // 进程资源使用统计数组。
unsigned int flags; // 各进程的标志(还未使用)。
unsigned short used_math; // 标志:是否使用了协处理器。
int tty; // 进程使用 tty 终端的子设备号。-1 表示没有使用。
unsigned short umask; // 文件创建属性屏蔽位。
struct m_inode * pwd; // 当前工作目录 i 节点结构指针。
struct m_inode * root; // 根目录 i 节点结构指针。
struct m_inode * executable; // 执行文件 i 节点结构指针。
struct m_inode * library; // 被加载库文件 i 节点结构指针。
unsigned long close_on_exec; // 执行时关闭文件句柄位图标志。(参见 include/fcntl.h)
struct file * filp[NR_OPEN]; // 文件结构指针表,最多 32 项。表项号即是文件描述符的值。
// 32位
struct desc_struct ldt[3]; // 局部描述符表。0-空,1-代码段 cs,2-数据和堆栈段 ds&ss。
struct tss_struct tss; // 进程的任务状态段信息结构。
};

linux-0.11最多可以拥有64个进程,这些PCB保存在如下全局数组task

// kernel/sched.c  NR_TASKS = 64 在 include/linux/sched.h 中
// init_task 为第一个进程,也是手动创建的进程
struct task_struct * task[NR_TASKS] = {&(init_task.task), };

进程切换时,会选出一个新的下标n,就是下一个进程在task数组中的下标值。然后,全局变量current指向新进程的数组下标值,然后切换新进程的tss数据结构。

2. tss_struct

也称为任务控制块TCB,保存在task_struct中,每个任务都有不同的TCBLDT

任务切换操作示意图

任务切换操作示意图

进程切换时,当current指向新进程后,相应的tss也要进行切换,恢复成新进程所保存的寄存器的值。

  1. tss_struct的初始化过程

这时就需要从头查看一个新进程的建立过程,主要为fork.c部分的代码。

fork为一个系统调用,sys_fork是主要执行程序。在sys_fork中会调用两个函数find_empty_processcopy_processfind_empty_process会返回一个可用的下标,copy_process则用于初始化新进程的一些结构。

// kernel/fork.c
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;

// 为新任务分配内存
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p;
// 复制进程结构
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
// 初始化task_struct和tss_struct
p->state = TASK_UNINTERRUPTIBLE;
.
.
.
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++)
if ((f=p->filp[i]))
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}

上面程序中的set_ldt_descset_tss_desc是两个比较重要的函数,用于在GDT表中设置新进程的TSSLDT

这部分函数源码如下:

// include/asm/system.h
#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
"movw %%ax,%2\n\t" \
"rorl $16,%%eax\n\t" \
"movb %%al,%3\n\t" \
"movb $" type ",%4\n\t" \
"movb $0x00,%5\n\t" \
"movb %%ah,%6\n\t" \
"rorl $16,%%eax" \
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)

#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x82")

3. 进程切换

在现在的 Linux 0.11 中,真正完成进程切换是依靠任务状态段(Task State Segment,简称 TSS)的切换来完成的。 Intel架构不仅提供了 TSS 来实现任务切换,而且只要一条指令就能完成这样的切换,即ljmp指令。

具体的工作过程是:

  1. 首先用 TR 中存取的段选择符在 GDT 表中找到当前 TSS 的内存位置。
  2. 找到了当前的 TSS 段以后,将 CPU 中的寄存器映像存放到这段内存区域中,即拍了一个快照。
  3. 存放了当前进程的执行现场以后,接下来要找到目标进程的现场,并将其扣在 CPU 上,找目标 TSS 段的方法也是一样的,因为找段都要从一个描述符表中找,描述 TSS 的描述符放在 GDT 表中,所以找目标 TSS 段也要靠 GDT 表,当然只要给出目标 TSS 段对应的描述符在 GDT 表中存放的位置即段选择子就可以了。
  4. 一旦将目标 TSS 中的全部寄存器映像扣在 CPU 上,就相当于切换到了目标进程的执行现场了,因为那里有目标进程停下时的 CS:EIP,所以此时就开始从目标进程停下时的那个 CS:EIP 处开始执行,现在目标进程就变成了当前进程,同时 TR 需要修改为目标 TSS 段在 GDT 表中的段描述符所在的位置, TR 总是指向当前 TSS 段的段描述符所在的位置。

上面给出的这些工作都是一句长跳转指令ljmp段选择子:段内偏移,在段选择子指向的段描述符是 TSS 段时 CPU 解释执行的结果,所以基于 TSS 进行进程/线程切换的switch_to实际上就是一句ljmp指令:

#define switch_to(n) {
struct{long a,b;} tmp;
__asm__(
"movw %%dx,%1"
"ljmp %0" ::"m"(*&tmp.a), "m"(*&tmp.b), "d"(TSS(n)
)
}

#define FIRST_TSS_ENTRY 4

// n * 16 + 第一个任务的位置 * 8
#define TSS(n) (((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3))

二、基于内核栈的进程切换

1. Linux中的进程与线程

2. 线程切换的过程

3. 我们要实现的部分

注:本文中所有内容都参考自赵炯博士写的《Linux内核完全注释》