结合源码的操作系统学习记录(1)--进程初识
2022/3/21 7:32:18
本文主要是介绍结合源码的操作系统学习记录(1)--进程初识,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
主要写一下进程和线程的相关学习,直接从用户态开始写,内核的知识需要时在补充。
-
进程的数据结构
在 linux kernel 中,进程通常被称为 task,内核通过进程表对进程进行管理,每个进程在进程表中占有一项。进程表项是一个 task_struct 的指针,也被称为进程控制块(PCB)或进程描述符(PD),其中保存着控制和管理进程的所有信息。
看一下 task_struct:
struct task_struct { /* these are hardcoded - don't touch */ long state; /*任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)。 */ long counter; // 任务运行时间计数(递减)(滴答数),运行时间片。 long priority; // 运行优先数。任务开始运行时counter = priority,越大运行越长。 long signal; // 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1。 struct sigaction sigaction[32]; // 信号执行属性结构,对应信号将要执行的操作和标志信息。 long blocked; /* 进程信号屏蔽码(对应信号位图)。 */ /* various fields */ int exit_code; // 任务执行停止的退出码,其父进程会取。 unsigned long start_code, end_code, end_data, brk, start_stack; // 代码段地址,代码长度(字节数),代码长度 + 数据长度(字节数),总长度(字节数),堆栈段地址 long pid, father, pgrp, session, leader; // 进程号,父进程号,父进程组号,会话号,会话首领 unsigned short uid, euid, suid; // 用户标识号,有效用户id,保存的用户id unsigned short gid, egid, sgid; // 组标识号,有效组id,保存的组id long alarm; // 报警定时值(滴答数) long utime, stime, cutime, cstime, start_time; // 用户态运行时间(滴答数),系统态运行时间(滴答数),子进程用户态运行时间,子进程系统态运行时间,进程开始运行时刻 unsigned short used_math; // 标志:是否使用了协处理器。 /* file system info */ int tty; /* 进程使用tty 的子设备号。-1 表示没有使用 */ unsigned short umask; // 文件创建属性屏蔽位 struct m_inode *pwd; // 当前工作目录i 节点结构 struct m_inode *root; // 根目录i 节点结构 struct m_inode *executable; // 执行文件i 节点结构 unsigned long close_on_exec; // 执行时关闭文件句柄位图标志 struct file *filp[NR_OPEN]; // 进程使用的文件表结构 /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */ struct desc_struct ldt[3]; // 本任务的局部表描述符 /* tss for this task */ struct tss_struct tss; // 本进程的任务状态段信息结构 };
关于 TSS:
task state segment,是操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。
代码实现:
有这个东西其实也很理所当然,当需要发生任务切换的时候,当前的寄存器都要给被切换的任务使用,这时有一个东西能将当前寄存器存储的内容保存下来,当任务切回来的时候,就可以从中翻出来任务切换前的旧状态,进而继续执行。
-
进程的运行状态
在刚刚我们提到了进程(任务)切换,这也就引发了一个新的问题,对于被切换掉的进程和当前的进程,CPU 需要判断它要执行哪个进程,这就需要给不同的进程标注不同的状态,方便 CPU 识别。
在 linux 中进程有这些状态:
- 运行状态:当进程正在被 CPU 执行,或已经准备就绪随时可有调度程序执行(一个新进程刚被创建后就处于本状态)
- 可中断睡眠状态:系统不会调度该进程执行,但当系统产生一个中断或者释放了进程正在等待的资源,或进程收到一个信号,都可以唤醒并转换到就绪状态。
- 不可中断睡眠状态:和可中断睡眠状态类似,但只有必须使用 wake_up() 函数唤醒
- 暂停状态:又特殊信号决定进入暂停还是由暂停转换到可运行。(在 linux0.11 中未实现)
- 僵死状态:当前进程以停止运行,但其父进程还没调用 wait() 查询其状态,也就是说当前进程的 task_struct 还被保留着。
-
进程初始化
众所周知新进程都是由父进程 fork 出来的,在完成内核初始化后,内核将执行权切换到用户模式(进程0),也就是 0 ring -> 3 ring,然后系统第一次调用 fork 函数,创建出一个用于运行 init() 的子进程(进程1)。
在 init/main.c 中,初始化部分:
mem_init(main_memory_start,memory_end); trap_init(); // 陷阱门(硬件中断向量)初始化。(kernel/traps.c) blk_dev_init(); // 块设备初始化。(kernel/blk_dev/ll_rw_blk.c) chr_dev_init(); // 字符设备初始化。(kernel/chr_dev/tty_io.c)空,为以后扩展做准备。 tty_init(); // tty 初始化。(kernel/chr_dev/tty_io.c) time_init(); // 设置开机启动时间 -> startup_time。 sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c) buffer_init(buffer_memory_end);// 缓冲管理初始化,建内存链表等。(fs/buffer.c) hd_init(); // 硬盘初始化。(kernel/blk_dev/hd.c) floppy_init(); // 软驱初始化。(kernel/blk_dev/floppy.c) sti(); // 所有初始化工作都做完了,开启中断。
转变为用户态(特权转换细节之后再说),并 fork 进程 1
move_to_user_mode(); // 移到用户模式。(include/asm/system.h) if (!fork()) { /* we count on this going ok */ init(); }
之后系统进入死循环,反复查看是否有其它任务可以运行,有就切过去,没有就继续 pause()
for(;;) pause();
-
新进程的产生
重点看一下 fork 函数的实现,首先执行 find_empty_process 获取一个可用的 id 和 pcb。
int find_empty_process(void) { int i; repeat: // 先找到一个可用的pid if ((++last_pid)<0) last_pid=1; for(i=0 ; i<NR_TASKS ; i++) if (task[i] && task[i]->pid == last_pid) goto repeat; // 再找一个可用的pcb项,从1开始,0是init进程 for(i=1 ; i<NR_TASKS ; i++) if (!task[i]) // 任务0 排除在外。 return i; return -EAGAIN; }
然后调用 copy_process 生气一页内存页来复制父进程的信息,并为新进程修改复制的 task_struct 结构的某些字段值,让新进程的状态报纸在父进程即将进入终端前的状态。
值得一提是,内核并不会立刻为新进程分配代码和数据内存页,新进程将与父进程共同使用父进程的代码和数据内存页,知道有另一个进程以写的方式访问这个新进程的时候,会触发写保护异常,被访问的页面才会被复制到新申请的内存页面中,也就是写时复制。
为新的 task_struct 分配内存,并加到进程表(任务数组)中
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; struct i387_struct *p_i387; p = (struct task_struct *) get_free_page (); // 为新任务数据结构分配内存。 if (!p) // 如果内存分配出错,则返回出错码并退出。 return -EAGAIN; task[nr] = p; // 将新任务结构指针放入任务数组中。
然后就是初始化 task_struct 结构,
*p = *current; /* 注意!这样做不会复制超级用户的堆栈 (只复制当前进程内容)。*/ p->state = TASK_UNINTERRUPTIBLE; // 将新进程的状态先置为不可中断等待状态。 p->pid = last_pid; // 新进程号。由前面调用find_empty_process()得到。 p->father = current->pid; // 设置父进程号。 p->counter = p->priority; p->signal = 0; // 信号位图置0。 p->alarm = 0; p->leader = 0; /* process leadership doesn't inherit */ /* 进程的领导权是不能继承的 */ p->utime = p->stime = 0; // 初始化用户态时间和核心态时间。 p->cutime = p->cstime = 0; // 初始化子进程用户态和核心态时间。 p->start_time = jiffies; // 当前滴答数时间。 // 以下设置任务状态段TSS 所需的数据(参见列表后说明)。 p->tss.back_link = 0; p->tss.esp0 = PAGE_SIZE + (long) p; // 堆栈指针(由于是给任务结构p 分配了1 页 // 新内存,所以此时esp0 正好指向该页顶端)。 p->tss.ss0 = 0x10; // 堆栈段选择符(内核数据段) p->tss.eip = eip; // 指令代码指针。 p->tss.eflags = eflags; // 标志寄存器。 p->tss.eax = 0; p->tss.ecx = ecx; p->tss.edx = edx; p->tss.ebx = ebx; p->tss.esp = esp; p->tss.ebp = ebp; p->tss.esi = esi; p->tss.edi = edi; p->tss.es = es & 0xffff; // 段寄存器仅16 位有效。 p->tss.cs = cs & 0xffff; p->tss.ss = ss & 0xffff; p->tss.ds = ds & 0xffff; p->tss.fs = fs & 0xffff; p->tss.gs = gs & 0xffff; p->tss.ldt = _LDT (nr); // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。 p->tss.trace_bitmap = 0x80000000;
如果当前任务使用了协处理器,就保存其上下文。
p_i387 = &p->tss.i387; if (last_task_used_math == current) _asm{ mov ebx, p_i387 clts fnsave [p_i387] }
设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为该新任务分配的内存页。
if (copy_mem (nr, p)) { // 返回不为0 表示出错。 task[nr] = NULL; free_page ((long) p); return -EAGAIN; }
如果父进程中有文件是打开的,则将对应文件的打开次数增1
for (i = 0; i < NR_OPEN; i++) if (f = p->filp[i]) f->f_count++;
将当前进程(父进程)的pwd, root 和executable 引用次数均增1
if (current->pwd) current->pwd->i_count++; if (current->root) current->root->i_count++; if (current->executable) current->executable->i_count++;
在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
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; // 最后再将新任务设置成可运行状态,以防万一
最后返回新进程号(不是任务号)
return last_pid;
跟进一下 copy_mem 函数,首先取局部描述符表中代码段描述符项和数据段描述符项中段限长
int copy_mem (int nr, struct task_struct *p) { unsigned long old_data_base, new_data_base, data_limit; unsigned long old_code_base, new_code_base, code_limit; code_limit = get_limit (0x0f); data_limit = get_limit (0x17);
然后分别取代码段和数据段在线性地址中的基址:
old_code_base = get_base (current->ldt[1]); old_data_base = get_base (current->ldt[2]);
0.11 不支持代码和数据段分立(I&D)
if (old_data_base != old_code_base) // 0.11 版不支持代码和数据段分立的情况。 panic ("We don't support separate I&D"); if (data_limit < code_limit) // 如果数据段长度 < 代码段长度也不对。 panic ("Bad data_limit");
然后设置进程的线性地址的首地址,每个进程占64MB,那么基址 = 任务号*64Mb(任务大小),同时设置代码段描述符和数据段描述符中基址
new_data_base = new_code_base = nr * 0x4000000; p->start_code = new_code_base; set_base (p->ldt[1], new_code_base); set_base (p->ldt[2], new_data_base);
最后把父进程的页目录项和页表复制到子进程
if (copy_page_tables (old_data_base, new_data_base, data_limit)) { // 复制代码和数据段。 free_page_tables (new_data_base, data_limit); // 如果出错则释放申请的内存。 return -ENOMEM; } return 0; }
下一篇更进程调度。
-
参考文献
- Linux内核完全注释
- https://www.zhihu.com/column/c_1094189343643652096
- https://github.com/sunym1993/flash-linux0.11-talk
- https://github.com/beride/linux0.11-1
这篇关于结合源码的操作系统学习记录(1)--进程初识的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-12如何创建可引导的 ESXi USB 安装介质 (macOS, Linux, Windows)
- 2024-11-08linux的 vi编辑器中搜索关键字有哪些常用的命令和技巧?-icode9专业技术文章分享
- 2024-11-08在 Linux 的 vi 或 vim 编辑器中什么命令可以直接跳到文件的结尾?-icode9专业技术文章分享
- 2024-10-22原生鸿蒙操作系统HarmonyOS NEXT(HarmonyOS 5)正式发布
- 2024-10-18操作系统入门教程:新手必看的基本操作指南
- 2024-10-18初学者必看:操作系统入门全攻略
- 2024-10-17操作系统入门教程:轻松掌握操作系统基础知识
- 2024-09-11Linux部署Scrapy学习:入门级指南
- 2024-09-11Linux部署Scrapy:入门级指南
- 2024-08-21【Linux】分区向左扩容的方法