一, 进程描述符
进程控制块 PCB: 是 OS 控制进程运行用的数据结构, 是一个 task_struct 结构体.
PCB 包括: 进程标识信息(进程标识符 PID 等), 执行现场信息(CPU 现场, 进程切换时需要保存现场信息), 进程映像信息(进程地址空间, 即进程在运行时代码, 数据, 栈放在什么位置, 方便 OS 对地址空间进行管理)(现场与地址空间比较重要), 进程资源信息, 信号信息.
对 PCB, 说其中几个重要的字段:
mm_struct: 有一个成员 mm, 标明了进程的地址空间;
thread: 记录了进程的现场, 最后一个字段;
thread_info 在 4.4.6 版本中改成了 stack, 包括内核栈 (即进程进入内核工作时需要的栈和用户栈是分开的) 和一些需要快速访问的数据.
在 4.4.6 版本中, stack 占用了两个页面, 即 8k, 大部分是放内核栈的, 低端约 10k 存放快速访问的信息. CPU 若想访问当前进程的快速访问数据的话, 只需要拿到当前的栈指针, 即 ESP 寄存器的值, 可以推算出数据所在的位置来, 因此在查找他的地址的时候, 访问速度可以很快. 这部分数据可以看作是进程描述符的一部分, 在空间上不是连续的, 但相互之间有指针, 可以相互找得到.
进程状态转换图, 可自行搜索.
在 4.4.6 中, 增加了被跟踪和僵死撤销状态.
进程描述符是管理进程的重要数据结构, 故他的组织方式非常重要. 0 号进程的描述符是由 init_task 这个变量所存储的. 从他出发, 所有进程描述符构成了双向链表. task_struct 中包含一个成员, 叫 tasts,tasks 类型是 list_head 类型, tasts 本身是嵌入在进程描述符里面的, 知道 tasks 的地址, 只要送减去 620 就能得到进程描述符的首地址. 在 Linux 中有很多这样的技巧, 即通过嵌入的地址, 反推结构体的地址, 进而找到结构体的其他成员.
进程与线程关系
多个线程构成线程组, 共享内存, 不共享栈.
一个会话对应一个终端, 在终端中敲一个命令相当于创建了一个进程组来执行.
下面进行演示, 建立一个文件命名为 0.gdb, 文件内容如下, 直接运行
- target remote localhost:1234
- dir ~/aos/lab/busybox
- add-symbol-file ~/aos/lab/busybox/busybox_unstripped 0x8048400
- display $lx_current().pid
- display $lx_current().comm
- b start_kernel
- b ls_main
- c
执行含有下列代码的文件
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
- void loop(){
- while(1);
- }
- void *p1(){
- printf("thread-1 starting\n");
- loop();
- }
- void *p2(){
- printf("thread-2 starting\n");
- loop();
- }
- void main(){
- int pid1, pid2;
- pthread_t t1,t2;
- void *thread_result;
- printf("main starting\n");
- if (!(pid1 = fork())){
- printf("child-1 starting\n");
- loop();
- exit(0);
- }
- if (!(pid2 = fork())){
- printf("child-2 starting\n");
- loop();
- exit(0);
- }
- pthread_create(&t1, NULL, p1, NULL);
- pthread_create(&t2, NULL, p2, NULL);
- pthread_join(t1, &thread_result);
- pthread_join(t2, &thread_result);
- int status;
- waitpid(pid1, &status, 0);
- waitpid(pid2, &status, 0);
- printf("main exiting\n");
- exit(0);
- }
可以看到 do-fork 可执行文件创建了三个进程, 976,977,978
979,980 是新创建的两个线程
再执行一次, 可以看到后台运行了两个
新创建的三个进程是刚创建的
fg % 序号将指定的进程放到前台, ctrl+z 放到后台
用以下三条命令依次查看线程组, 进程组和会话的 leader
命令的意思是根据进程的描述符, 找到线程组 leader 的描述符, 里面对应的字段就是要显示的 ID
- p $lx_task_by_pid(977).group_leader->pids[0].pid->numbers.nr
- p $lx_task_by_pid(977).group_leader->pids[1].pid->numbers.nr
- p $lx_task_by_pid(977).group_leader->pids[2].pid->numbers.nr
987 和 988 是 984 创建的, 他们处于同一个线程组, leader 是 976
二, 进程调度算法
每个进程属于某一个调度器类, 每个调度器类都有一个进程队列, 不同的队列有不同的调度算法.
先调度硬实时的, 软实时次之, 普通进程最后.
普通进程使用 CFS(完全公平)调度算法:
虚拟时钟, 调度器总是选时钟最小的那个进程来执行.
优先级高的进程时钟增长得慢.
所有可运行的进程被放在一个红黑树中.
下面进行演示:
再次运行 0.gdb, 在终端输入 ls, 使其被捕获
建立文件 demo-2-2.gdb, 内容如下
- break __schedule// 进程调度的时候执行这个函数
- break __switch_to// 调度时如果切换进程就会调用这个函数
- commands
- printf "next_p->pid: %d\n", next_p->pid
- printf "next_p->se.vruntime:"
- print next_p->se.vruntime
- end
- break enqueue_task_fair// 如果有新进程要进入到 CFS 队列时, 执行这个函数
- commands
- printf "p->pid: %d\n", p->pid
- printf "p->se.vruntime:"
- print p->se.vruntime
- end
- display $lx_current().state// 显示当前进程的状态
- display $lx_current().se.vruntime
- display $lx_per_cpu("runqueues").nr_running//CPU 里面有多少个进程在运行
- display ((struct sched_entity *)((void *)$lx_per_cpu("runqueues").cfs.rb_leftmost - 0x8))->vruntime//CFS 队列里最左边的节点, 即虚拟时钟最小的信息
- display ((struct task_struct *)((void *)$lx_per_cpu("runqueues").cfs.rb_leftmost - 0x4c))->pid
由上图可以看到, 当前正在运行的是 975 号进程, 当前的虚拟时钟可以从 runtime 那里看到, state=0 表示其当前的状态是就绪的或正在运行, 树最左边目前还没有进程. 由于 enqueue_task_fair 函数的作用是往进程队列里面加入新进程, 现在已经有一个, 可以看到, 要加的是 7 号进程, 下面一行的虚拟时钟是个负值, 现在还暂时看不到, 继续执行.
可以看到 7 号进程的虚拟时钟小于 975 号的, 下次如果要调度, 应该选 7 号. 即将创建的是 3 号进程.
由上图, 975 号进程的虚拟时钟增加了, 在这两个断点时间, 存在中断, 这才导致了时钟的增加. 运行有一定的随机性, 虚拟机在虚拟的时候有一定的随机性. 继续
运行了切换函数, 下一步要切换 7 号进程. 继续
7 号时钟的进程的时钟比之前也增加了, 需要注意当前正在运行的进程不放在树里面, 但放在了队列里面, 队列里面进程就是树里面的进程加当前进程. 继续若干次
下一步要创建的是 4 号进程.
除了普通进程有队列之外, 其他的硬实时和软实时都有各自的队列.
红黑树的某个节点可以是另外一个树, 共用一个时钟.
三, 进程调度的时机
内核程序的入口, 系统调用总控函数, 异常处理函数, 中断处理函数, 内核线程主函数, 用 bt 查看栈顶层, 根据函数的种类来确定是哪种内核调用.
来源: https://www.cnblogs.com/ppbb/p/12494794.html