PART 1
- 问:进程和线程的关系是什么?
- 答:进程=线程+资源
本书第一节用非常冗长的语言描述这个问题。但总而言之归纳几点就是:
- 线程是操作系统调度的基本单位
- 进程=线程+资源
- 线程是一个 “执行流” 概念,不需要过多去解读这个词语。
- 出于上面一点,能将所有程序分为 “单线程进程” 和 “多线程进程”。即普通的未使用线程功能的程序也能算作 “单线程进程”
- Linux系统下称进程为 “任务”(Task) ,但进程和线程是概念性的事物,而任务是实现上的结果,不需要过度去在意其称呼。
- Linux下的线程实现来自于POSIX线程库,自Linux2.6以后,因为NPTL的成功,该方案支持的线程的内核级的。只有一些古老的版本会有用户级线程
本节也提到了所谓 “上下文” 的概念:程序代码执行所依赖的 寄存器映像 和 内存资源。后者一般指的是堆和栈。
PART 2
本章后半部分笔者曾在《深入Linux内核架构》中了解到些许,但对其实现十分费解,这次算是对实现也清楚一些了。但本章还留有一些问题,本书作者表示会在chapter 10解决它,但我目前还不清楚我遇到的问题是不是就是作者所说的,或许有一点偏差,但具体还要等笔者看完第十章再做评价。
首先是Linux架构里所用的“任务”PCB:(注释就不删了,个人觉得还对本章掌握的有点模糊,留着以后有问题了再看)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct task_struct { uint32_t* self_kstack; // 各内核线程都用自己的内核栈 enum task_status status; char name[16]; uint8_t priority; uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数
/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, * 也就是此任务执行了多久*/ uint32_t elapsed_ticks;
/* general_tag的作用是用于线程在一般的队列中的结点 */ struct list_elem general_tag;
/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */ struct list_elem all_list_tag;
uint32_t* pgdir; // 进程自己页表的虚拟地址 uint32_t stack_magic; // 边界标记,用于检测栈的溢出 };
|
内核为支持多任务需要自己维护一张链表来让任务间能够切换。以PCB中的ticks代表时间片,每次时钟中断时将消减当前PCB中的时间片,在为0时进行一次调度(此前需要先关闭中断,以防止调度器被自己调度)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void schedule() { ASSERT(intr_get_status() == INTR_OFF); struct task_struct* cur = running_thread(); if (cur->status == TASK_RUNNING) { ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); list_append(&thread_ready_list, &cur->general_tag); cur->ticks = cur->priority; cur->status = TASK_READY; } else { }
ASSERT(!list_empty(&thread_ready_list)); thread_tag = NULL; // thread_tag清空 /* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */ thread_tag = list_pop(&thread_ready_list); struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); next->status = TASK_RUNNING; switch_to(cur, next); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| switch_to: ;栈中此处是返回地址 push esi push edi push ebx push ebp
mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20] mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段, ; self_kstack在task_struct中的偏移为0, ; 所以直接往thread开头处存4字节便可。 ;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ---------------- mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24] mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针, ; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针 pop ebp pop ebx pop edi pop esi ret ; 返回到上面switch_to下面的那句注释的返回地址, ; 未由中断进入,第一次执行时会返回到kernel_thread
|
调度函数中用作切换的switch_to是由汇编语言编写的。其过程为:
保存现场 > 切换栈帧 > 恢复现场 > 返回线程
之所以ret对应了返回地址,是因为上一个线程在被调度时调用了schedule将自身保存在自己的栈中,在切换回原本的栈帧以后便能够重新恢复。
注:kernel_thread中会先打开中断,然后跳转到对应线程。
不过因为内核自己也是一个进程,所以在开始调度之前应该先为内核本身生成PCB:
1 2 3 4 5 6
| static void make_main_thread(void) { main_thread = running_thread(); init_thread(main_thread, "main", 31); ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag)); list_append(&thread_all_list, &main_thread->all_list_tag); }
|
另外再注册时钟中断函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| static void intr_timer_handler(void) { struct task_struct* cur_thread = running_thread(); ASSERT(cur_thread->stack_magic == 0x19870916); // 检查栈是否溢出
cur_thread->elapsed_ticks++; // 记录此线程占用的cpu时间嘀 ticks++; //从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数
if (cur_thread->ticks == 0) { // 若进程时间片用完就开始调度新的进程上cpu schedule(); } else { // 将当前进程的时间片-1 cur_thread->ticks--; } }
|
多嘴一句,中断默认情况下是关闭的。且每次进入中断以后,处理器会自动关中断,直到执行“iret”指令或者手动开启中断(本质上应该是恢复eflags寄存器)。
所以schedule函数第一行能够成立:
1
| ASSERT(intr_get_status() == INTR_OFF);
|
一般的中断调用结束时会调用iret指令恢复eflags寄存器来重开中断。所以一个隐蔽的情况是:(其实调试一下应该就能明白)
调度程序的switch_to函数第一次调度时返回到kernel_thread,在该函数中开启中断;而在此后的调度中,会返回到jmp intr_exit指令出,在之后的iret指令下恢复eflags寄存器,从而开启中断。
插画ID:75919964