Linux 操作系统简介
Linux 拥有现代操作系统的功能, 如真正的抢先式多任务处理, 支持多用户内存, 保护虚拟内存, 支持 SMP,UP, 符合 POSIX 标准联网, 图形用户接口和桌面环境具有快速性, 稳定性等特点. 本文通过分析 Linux 内核源代码, 说明了 Linux 作为操作系统内核是如何对进程组织, 转换, 调度的.
进程的组织
进程在内核中是以 PCB 的形式存在的, 一个操作系统有很多进程, 即就是很多 PCB 同时存在, 如何有效的管理进程的组织方式, 显然是一个很重要的问题, 为了遍历内核中所有的进程, 内核将这些 PCB 直接用双向链表链起来, task_struct 结构中的 tasks 字段就是用来连接所有进程描述符的. 在这个链中, 链表头是 init_task 描述符, 它是所谓的 0 进程或者 swapper 进程的描述符, init_task.tasks.prev 字段指向链表中最后插入的进程描述符的 tasks 字段.
- struct task_struct {
- struct list_head run_list;
- struct list_head tasks;
- struct task_struct *real_parent; /*real parent process (when being debugged) */
- struct task_struct *parent; /* parent process */
- struct list_head children; /* list of my children */
- struct list_head sibling; /* linkage in my parent's children list */
- struct task_struct *group_leader; /* threadgroup leader */
- /* PID/PID hash table linkage. */
- struct pid_link pids[PIDTYPE_MAX];
- struct list_head thread_group;
- };
从以上源代码中的字段可知 Linux 是如何组织进程的.
字段 run_list:
在内核中有一个队列是可运行队列, 即就是保存了进程状态为: TASK_RUNNING 的进程 PCB, 这个队列就是 run_list 字段, 内核为每个优先级都设置了一个可运行队列, 即如果一个进程的优先级为 k(取值范围是 0 到 139), 那么内核就将此进程链入到优先权为 k 的可运行队列中, 这样 CPU 可以从高优先级可可运行队列去选择要运行的进程, 这样提高了效率, 但为此却要把所有可运行的进程队列拆分成 140 个不同的队列. 这些队列有一个单独的数据结构 prio_array_t 数据结构 来实现, 定义如下 (摘自 linux2.6.11):
- typedef struct prio_array prio_array_t;
- struct prio_array {
- unsigned int nr_active;
- unsigned long bitmap[BITMAP_SIZE];
- struct list_head queue[MAX_PRIO];
- };
字段 children, sibling 和 thread_group:
这三个字段分别是进程的子进程链表, 兄弟链表和线程组链表. 可以根据 children 和 sibling 字段找到一个进程家族的进程. 进程在创建后必须处于某一个组中, 通过 thread_group 来形成一个进程组.
进程的转换
Linux 中进程切换过程与函数调用堆栈基本类似
- void my_schedule(void)
- {
- tPCB * next;
- tPCB * prev;
- if(my_current_task == NULL
- || my_current_task->next == NULL)
- {
- return;
- }
- printk(KERN_NOTICE ">>>my_schedule<<<\n");
- /* schedule */
- next = my_current_task->next;
- prev = my_current_task;
- /* 进程上下文的切换, 与函数调用堆栈类似 */
- if(next->state == 0)/* -1 unrunnable, 0 runnable,>0 stopped */
- {
- /* switch to next process */
- asm volatile(
- /* 保存当前进程的 ebp esp*/
- "pushl %%ebp\n\t" /* save ebp */
- "movl %%esp,%0\n\t" /* save esp */
- /* 构建下一个进程的 esp*/
- "movl %2,%%esp\n\t" /* restore esp */
- /*$1f 是指下面标号为 1 的位置, 就是下一个进程启动的位置 */
- "movl $1f,%1\n\t" /* save eip */
- /* 下一个进程 eip 压栈 */
- "pushl %3\n\t"
- /* 恢复现场: eip 指向下下个进程的地址 ebp 恢复为第一条指令压栈保存的 ebp*/
- "ret\n\t" /* restore eip */
- "1:\t" /* next process start here */
- "popl %%ebp\n\t"
- : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
- : "m" (next->thread.sp),"m" (next->thread.ip)
- );
- my_current_task = next;
- printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
- }
- else
- {
- next->state = 0;// 新的进程设置为正在运行状态
- my_current_task = next;
- printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
- /* switch to new process */
- asm volatile(
- "pushl %%ebp\n\t" /* save ebp */
- "movl %%esp,%0\n\t" /* save esp */
- /* 新的进程从来没有执行过, 所以栈为空 esp 与 ebp 指向同一位置 */
- "movl %2,%%esp\n\t" /* restore esp */
- "movl %2,%%ebp\n\t" /* restore ebp */
- "movl $1f,%1\n\t" /* save eip */
- "pushl %3\n\t"
- "ret\n\t" /* restore eip */
- : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
- : "m" (next->thread.sp),"m" (next->thread.ip)
- );
- }
- return;
- }
切换过程:
1. 保存当前进程的堆栈环境 (esp,eip,ebp), 首先将当前进程的 ebp 压栈, 并将 esp 和 eip 保存在当前进程的结构体中
2. 构建下一进程的堆栈环境. 然后下一条进程的 thread.sp 读出, 赋给 esp;thread.ip 读出赋给 eip. 若下一条进程是新的进程, 处于未运行状态, 则新进程堆栈为空, esp 与 ebp 指向同一地址, 这种情况与函数调用堆栈更加类似. 若下一条进程处于运行态, 那么 ebp 与当前进程的 ebp 指向同一地址.
3. 恢复堆栈环境, 为下下个进程做准备. 恢复 eip, 即把执行完成的进程的 thread.ip(下一进程的指令地址) 赋给 eip, 上例中所有进程的 thread.ip 设为 my_process.
如图所示:
进程的调度
Linux 系统进程提供了两种优先级, 一种是普通的进程优先级, 第二个是实时优先级. 前者适用 SCHED_NORMAL 调度策略, 后者可选 SCHED_FIFO 或 SCHED_RR 调度策略. 任何时候, 实时进程的优先级都高于普通进程, 实时进程只会被更高级的实时进程抢占, 同级实时进程之间是按照 FIFO(一次机会做完) 或者 RR(多次轮转) 规则调度的.
Linux 进程状态机:
个人看法
在日常使用手机或者电脑的时候, 经常在将应用关闭或退出后, 仍然还有许多进程在后台运行, 促使我对进程的存在产生了好奇心. 现在明白, 不仅仅在应用运行时会产生进程, 计算机系统本身在运行过程中也会产生许多进程, 就是这些进程使得计算机可以调用许多功能, 与人进行交互. 而通过对 Linux 操作系统进程的探索, 使我对进程有了更进一步的了解, 懂得了为什么计算机可以同时运行那么多程序, 好的进程的组织, 转换和调用模式可以极大地提升计算机的性能, 帮助我们更好地完成任务.
来源: https://www.cnblogs.com/zhengzhihuang/p/8966846.html