进程的创建过程
------基于Linux0.11源码分析
1. 背景
进程的创建过程无疑是最重要的操作系统处理过程之一,很多书和教材上说的最多的还是一些原理的部分,忽略了很多细节。比如,子进程复制父进程所拥有的资源,或者子进程和父进程共享相同的物理页面,拥有自己的地址空间,子进程创建后接受统一调度执行等等。
原理性的书籍更多地关注了进程创建过程中各个关键部分的功能,但由于过于抽象,很难理解,因此如果自己能够实际操作,实践这个过程就很重要,可以让那些看起来抽象的概念变的现实而容易理解,比如所谓的父进程的资源,父进程所拥有的物理页面,甚至父进程的地址空间等等,这些抽象的概念其实只要实际操作一次就更能有感性的认识。本人参考Linux0.11源代码实践了创建进程和调度,这个过程获益匪浅,这里把主要的学习成果结合实践总结一下。
2. 0号进程
子进程的创建是基于父进程的,因此一直追溯上去,总有一个进程是原始的,即没有父进程的。这个进程在Linux中的进程号是0,也就是传说中的0号进程(可惜很多理论书上对这个重要的进程只字不提)。
如果说子进程可以通过规范的创建进程的函数(如:fork())基于父进程复制创建,那么0号进程并没有可以复制和参考的对象,也就是说0号进程拥有的所有信息和资源都是强制设置的,不是复制的,这个过程我称为手工设置,也就是说0号进程是“纯手工打造”,这是操作系统中“最原始”的一个进程,它是一个模子,后面的任何进程都是基于0号进程生成的。
手工打造0号进程最主要包括两个部分:创建进程0运行时所需的所有信息,即填充0号进程,让它充满“血肉”;二是调度0号进程的执行,即让它“动”起来,只有动起来,才是真正意义上的进程,因为进程本身实际上是个动态的概念。
不同的操作系统或者同一个操作系统的不同版本进程信息的内涵可能会有些细微的差距,但大体上关键的部分和逻辑是没有什么不同的,我这里只是基于Linux0.11的实现来描述进程创建的关键步骤和关键细节。
1)填充0号进程信息
进程包括的内容非常复杂,但总的来说进程的信息都是由进程的描述符引导标识的,因此填充0号进程的过程逻辑上是以填充其描述符为牵引完成的(也有书将进程描述符称为进程控制块)。下面是Linux0.11版进程的描述符信息结构体:
struct task_struct {
long state,counter,priority, signal;
struct sigaction sigaction[32];
long blocked;
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,gid,egid,sgid;
long alarm;
long utime,stime,cutime,cstime,start_time;
unsigned short used_math;
int tty;
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];
struct desc_struct ldt[3];
struct tss_struct tss;
};
可以看到进程描述符里的信息很多,大体上有几部分:
a. 进程的运行信息,如进程的当前状态(state),进程的各种时间片消耗记录(utime、stime等),进程的信号(signal)和优先级(priority)等。
b. 进程的基本创建信息,如进程号(pid),进程的创建用户(uid)等。
c. 进程的资源类信息,如使用的tty自设备号(tty),文件根目录i节点结构(root)等。
d. 进程执行和切换CPU需要使用的关键信息:局部描述符表(LDT)、任务状态段(TSS)信息。
这些信息并不是在进程创建的时候就全部确定的,大部分只是暂时赋一个初值,在运行的时候会动态更改,也有一些是要在进程运行前设置好的,才能保证进程被正确地执行起来。实际上,我们最需要填充的信息是那些使得操作系统可以顺利切换到0号进程的信息,最重要的显然是进程的LDT和TSS信息。TSS是CPU在切换任务时需要使用的信息,而LDT是局部描述符表,0号进程是第一个运行在用户态的进程,需要使用自己的LDT。TSS和LDT是保证不同进程之间相互隔离的重要机制。
实际上还有一个重要的信息不是放在进程本身的描述符里的,而是放在全局描述符表GDT中,因为所有的进程是由操作系统统一管理的,因此操作系统至少要保持对它们的索引,这种索引性质的信息放在操作系统内核的GDT中。对于Linux0.11来说,每个进程都有一个LDT和一个TSS描述符,而Linux2.4之后是每个CPU一个TSS描述符并存储在GDT中,而不是每个进程一个。当然这种区别会造成进程创建和切换过程中一些细节上的差异,但本质的部分和任务的切换过程并没有任何不同。
下面是Linux0.11手动填充进程0的进程描述符信息的宏:
#define INIT_TASK /
/* state etc */ { 0,15,15, /
/* signals */ 0,{{},},0, /
/* ec,brk... */ 0,0,0,0,0,0, /
/* pid etc.. */ 0,-1,0,0,0, /
/* uid etc */ 0,0,0,0,0,0, /
/* alarm */ 0,0,0,0,0,0, /
/* math */ 0, /
/* fs info */ -1,0022,NULL,NULL,NULL,0, /
/* filp */ {NULL,}, /
/* ldt */ { /
{0,0}, /
{0x9f,0xc0fa00}, /
{0x9f,0xc0f200}, /
}, /
/*tss*/ { 0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,/
0,0,0,0,0,0,0,0, /
0,0,0x17,0x17,0x17,0x17,0x17,0x17, /
_LDT(0),0x80000000, {} /
}, /
}
除了填充进程描述符的信息外,还需要在GDT中设置相关的项,即进程0的LDT和TSS选择符,这个工作是在sched_init()里完成的:
void sched_init(void){
...
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
...
ltr(0);
lldt(0);
}
可以看到,在进程0的TSS和LDT描述符信息设置到GDT中后,立刻设置了TR寄存器和LDTR寄存器,为即将运行0号进程作准备。
2)运行0号进程
进程0是运行在用户态下的进程,因此就意味着进程0的运行过程实际上是一个从0级特权级到3级特权级切换的过程,使用的是CPU指令iret,模拟了中断调用的返回过程,具体执行过程由move_to_user_mode完成:
#define move_to_user_mode() /
__asm__ ("movl %%esp,%%eax/n/t" /
"pushl $0x17/n/t" /
"pushl %%eax/n/t" /
"pushfl/n/t" /
"pushl $0x0f/n/t" /
"pushl $1f/n/t" /
"iret/n" /
"1:/tmovl $0x17,%%eax/n/t" /
...)
这个宏将进程0执行时的ss,esp,eflags.cs,eip信息全部压栈,待到执行iret指令时,CPU将这几项信息从栈中弹出加载到相应的寄存器中,这样就实现了进程0的启动执行。从这里也可以看出,进程0刚开始执行时几个关键寄存器的信息也是在其运行前事先设定好的,从进程描述符信息到执行信息均是人为设置,因此我称之为“纯手工打造的进程”。
3. 子进程的创建
有了0号进程这个原始的进程,再来看子进程的创建就比较容易理解一些。除了0号进程外,其余的进程均使用系统调用fork()完成,其具体工作由内核态的_sys_fork实现:
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
可以看到,一个进程的创建主要有两个步骤:一是找到一个空闲进程资源(find_empty_process),Linux0.11来说可以同时运行的进程数目是64个,是有限的,因此需要先得到一个空闲的进程表中的一项用来索引即将创建的进程信息;第二个主要步骤就是复制(copy_process),这个函数具体来实现子进程基于父进程的复制创建。
主要包括的步骤和内容是:
1) 为新进程在内存中分配一个物理页,将新进程的描述符信息填充在该页的开头,并设置新进程的描述符里各项信息;
2) 拷贝父进程的页表,使得它们共同指向相同的物理页,同时将父进程的各个页表属性改为只读,这样将来可以使用写时复制机制。
3) 在GDT中设置该进程项的TSS和LDT选择符。
Linux0.11版本子进程内容的设置主要内容就是这些,当然不同版本会有不同,在改进执行性能上也会有改进,但这个版本体现出来的最基本创建过程基本上反映了操作系统创建进程的主要过程。
4. 子进程的运行
子进程在创建好后并不能立即执行,至少需要一次调度,而这个调度到子进程的运行过程就完全不需要像进程0那样人为在栈上设置信息然后用iret方式,而是执行的任务的切换过程。不考虑进程调度的各个算法和选择细节,最终负责完成切换操作的函数如下:
#define switch_to(n) {/
struct {long a,b;} __tmp; /
__asm__("cmpl %%ecx,_current/n/t" /
"je 1f/n/t" /
"movw %%dx,%1/n/t" /
"xchgl %%ecx,_current/n/t" /
"ljmp %0/n/t" /
"cmpl %%ecx,_last_task_used_math/n/t" /
"jne 1f/n/t" /
"clts/n" /
"1:" /
::"m" (*&__tmp.a),"m" (*&__tmp.b), /
"d" (_TSS(n)),"c" ((long) task[n])); /
}
最终的切换执行了一个ljmp操作,它的操作数是一个任务描述符,这会导致CPU执行一次任务切换,根据新进程的TSS信息将相关信息加载进cs,eip,eflags,ss,esp寄存器开始执行新的代码。当然由于先前拷贝的父进程的相关页面被设置为只读,子进程第一次执行到该页面时会触发页保护的异常,这时会触发写时复制操作,为子进程分配自己的相应页面。
符:任务(task)和进程(process)的区别
任务和进程很容易被人混淆,甚至在Linux中进程描述符结构体也是用task_struct表示,而不是process,这更让人有的时候搞不清楚。我个人认为,其实任务的概念更底层,可以认为是基于CPU的角度来考虑的,进程所处的层次更高一些,应当可以认为是操作系统一级的概念。
任务关注点是一组程序操作,这组操作实现了某个功能,它最终会涉及到指令级别,我们说任务的切换最终需要关注的还是CPU的相关指令。
进程的概念通常是指程序的执行,是动态的过程。进程除了包含其要运行的程序之外,还包括运行时的诸多信息,如运行时间,信号等等。
分享到:
相关推荐
学习Linux内核解析,不仅需要理解基本的计算机科学概念,还需要熟悉C语言,因为Linux内核主要是用C语言编写的。此外,理解汇编语言也有助于更深入地了解内核的底层运作。通过深入研究内核,开发者可以优化系统性能,...
在《Linux内核设计与实现》第二版的学习笔记中,我们可以深入探讨以下几个关键知识点: 1. **内核架构**:Linux内核采用微内核架构,主要由进程管理、内存管理、文件系统、设备驱动和网络协议栈等模块组成。这些...
Linux内核是操作系统的核心部分,它负责管理计算机的硬件资源,提供给应用程序接口,并协调系统间的各个组件。内核的存在是为了解决操作系统的核心问题,包括处理硬件的直接交互、调度进程、管理内存、实现文件系统...
总之,Linux内核的学习涵盖了从其历史、构成、代码结构到配置编译等多个方面,理解这些知识点对于系统管理员、开发者以及任何对操作系统原理感兴趣的人来说都是至关重要的。通过深入研究和实践,可以更有效地定制和...
这份“Linux内核笔记-很强大很详细的”压缩包包含了两个PDF文档,分别是“joyfire的linux内核笔记.pdf”和“joyfire的linux系统管理笔记.pdf”,它们深入浅出地探讨了Linux内核的各个方面,对于想要深入理解Linux...
通过深入学习《Linux内核笔记》,开发者可以更好地理解操作系统底层工作原理,提高系统优化和调试能力,为开发高效、可靠的Linux应用打下坚实基础。同时,参与内核开发也是贡献开源社区、提升技术影响力的重要途径。
这份“Linux0.11 内核学习笔记”详细解读了这个早期内核的结构、工作原理以及相关的编程技术。下面,我们将深入探讨其中的关键知识点。 一、内核架构 Linux 0.11内核采用单内核设计,所有的系统服务都集中在一个可...
这篇“Linux内核学习笔记(2)——内存寻址”将深入探讨Linux内核如何管理内存以及内存寻址的基本原理。内存寻址是计算机科学中的核心概念,它涉及到计算机如何定位并访问存储在内存中的数据。 在Linux中,内存被...
0.11版本虽然相对于现代的Linux内核来说相当古老,但它包含了操作系统基础功能的雏形,如进程管理、内存管理、文件系统、设备驱动等关键模块,是学习操作系统原理和Linux内核开发的入门起点。 在《Linux内核完全...
本读书笔记整理了《深入理解Linux内核》的部分内容,旨在帮助新手理解Linux内核是如何通过硬件支持实现内存寻址和分页的。 首先,Linux的内存寻址使用逻辑地址,由两部分组成:段标识符(Segment Selector)和偏移...
### Linux内核笔记——进程管理_80386基础 #### 1. 80386保护模式下的进程管理基础知识 对于初学者来说,Linux内核的学习之路充满了挑战,尤其是在面对内核启动代码时,复杂的AT&T汇编语法与保护模式下的概念常常...
在学习笔记中,这部分内容可能会涵盖如何编写和调试驱动,以及理解Linux内核中的设备模型。 文件名“s3c2440中文用户手册(单PDF整合).pdf”可能包含的是Samsung S3C2440处理器的详细技术文档。S3C2440是一款基于ARM...
这份"Linux内核机制学习笔记带源码及代码注释.7z"压缩包包含了丰富的学习材料,可以帮助我们深入理解Linux内核的工作原理。下面我们将详细探讨其中涉及的一些关键知识点。 1. **内核启动与初始化**: - Linux内核...
以下是 Linux Kernel 的学习笔记,涵盖了存储器寻址、设备驱动程序开发、字符设备驱动程序、PCI 设备、内核初始化优化宏、访问内核参数的接口、内核初始化选项、内核模块编程和网络子系统等方面的知识点。...
不管在什么系统中,所有的任务都是以进程为载体的,所以理解进程的创建对于理解操作系统的原理是非常重要的,本文是我在学习linux内核中所做的笔记,如有错误还请大家批评指正。注:我所阅读的内核版本是0.11。 ...
在Linux操作系统中,进程...理解这些概念对于深入理解Linux内核的工作原理、进程管理和调度机制至关重要,有助于进行系统编程和性能优化。在实际操作中,这些知识点可用于进程控制、进程通信、信号处理以及调试等场景。
尚观Linux内核驱动开发笔记不仅涵盖了这些基础知识,还可能包含实践案例、常见问题解析以及高级技术探讨,例如异步I/O、内存管理优化、多线程同步等。通过学习和实践这些内容,开发者可以提升在Linux平台上的系统...
通常,Linux内核包括进程管理、内存管理、设备驱动、文件系统等多个模块。通过分析这张图,可以更好地定位驱动程序在整体架构中的位置。 3. 驱动程序分析: 这可能是对某个特定驱动程序的详细分析文档,包括其设计...
进程管理是操作系统的核心功能之一,这一章可能讲解了Linux内核中的进程模型,包括进程的创建、调度、同步和通信。读者可以学习到进程状态转换、上下文切换的概念,以及fork、exec和wait等系统调用的工作方式。进程...
Linux并非单一的操作系统,而是基于Linux内核的一系列发行版的统称,如Ubuntu、CentOS、Fedora等。它倡导开放源代码,鼓励社区协作,拥有丰富的软件资源和强大的开发工具。 二、Linux系统结构 Linux遵循分层设计,...