第3章 进程管理

进程管理

task_struct :

在32位机器上可以有1.7KB大小,存于任务队列的双向链表中(系统中存在多种调度机,所以这种任务队列存在多个)。

作用:内核管理进程所需要的一切信息,比如打开的文件,进程的地址空间(mm_struct),挂起的信号(作为链表,可见后续linux的数据结构),进程状态,上下文寄存器存储的信息等。

slab构造,可由位于进程内核栈底的thread_info指向,也可由r2寄存器指向(实时性指向,为了寻址更迅速)。

current宏返回当前的task_struct的指针(要么通过计算thread_info偏移,要么直接返回r2寄存器(当然这个寄存器专门存放当前进程描述符的地址,只有某些CPU体系有))。

进程状态

1.TASK_RUNNING;运行

2.TASK_INTERRUPTIBLE;  睡眠状态,可中断

3.TASK_UNINTERRUPTIBLE;  睡眠状态,不可中断

4._TASK_TRACED;  被跟踪

5._TASK_STOPPED;  停止(挂起)

通过set_task_state( task,state );  设置当前进程状态(尽量避免直接修改内核结构体,系统给定的函数是会在必要的时候设置内存屏障等保证安全)

进程家族树

内核在系统启动的最后阶段启动init进程作为始祖进程(PID为1),该进程读取系统初始化脚本以及执行其它相关程序,最终完成系统启动的整个过程。一个进程可以创建子进程(或者线程,两者的区别在于线程与父进程共享地址空间)。

每个进程可以通过进程描述符指向其父亲的task_struct,其所有的孩子的task_struct(通过子链),整个成为一个树状家族谱或者是全连通图,因此通过某个进程(线程)可以轻松地访问系统内的所有进程的进程描述符,但是不要这么做

进程生产过程

1.进程创建

首先,进程和线程(包括内核线程)的创建均通过系统调用(陷入内核态的方式,通过内中断软触发实现,有cpu体系的支持)触发的,会按序执行很多内核函数。

linux进程分为用户进程,用户线程,内核线程三种。

1.其中用户进程通过系统调用fork,vfork实现的(这个场景下父进程会休眠,等唤醒子进程后再唤醒父进程,目的:刚刚创建子进程的时候子进程与其父共享地址空间,如果父亲在调度会产生数据变化影响生成子进程后续的操作。)。

2.用户线程通过clone系统调用实现。

3.内核线程通过create_kthread来实现。

三种进程的创建过程均会调用

do_fork(unsigned long clone_flags,    //创建子进程相关的参数,决定了父子进程之间共享的资源种类,以及执行do_fork功能的不同

unsigned long stack_start,                   //进程栈开始地址(用于计算偏移之后来分配具体的进程内核栈)

unsigned long stack_size,                    //进程栈空间大小(防止分配空间超出)

int __user *parent_tidptr,                     //父进程的pid

int __user *child_tidptr,                        //子进程的pid(只是当前的命名的,还需要转为namespace下的)

unsigned long tls                                 //线程局部存储空间的地址(没有分配内核栈的暂存地)。

)

clone_flags参数

do_fork()执行过程:

1.根据clone_flags确定类型->2.调用 copy_process()函数(相当重要的初始化过程)->3.记录调度相关的东西->4.获取子进程id并将之转为namespace下的pid(namespace是虚拟化技术的重要部分)->5.初始化completion同步变量vfork ->6.唤醒刚创建的子进程    ->7.追踪子进程(初始化护航吧)->8.如果是vfork父进程休眠,则等待子进程将之唤醒    ->9.保存pid到namespace空间。

copy_process()执行过程:

1.检查clone_flags参数 ->2.调用dup_task_struct分配task_struct(使用slab)和分配内核栈(调用内核的伙伴系统分配,4K或8K(1页~2页左右,根据体系来)),创建完成之后父子进程描述符完全相同->3.task_struct简单配置初始化->4.初始化调度策略(决定使用的是CFS或者是RT),和优先级->5.根据clone_flags参数来决定重新分配或者是共享父进程的内容:打开的文件、文件系统信息、信号处理函数、进程地址空间、和命名空间等(调用一批函数)->6.初始化内核栈和thread_struct(保留一些PCB信息)结构->7.分配pid并加入到pid_hash中,之后返回task_struct。

写时拷贝:

fork()的实际开销就是复制父进程页表,和给子进程创建唯一的进程描述符。其余的尤其是地址空间里面大批量的数据是不需要复制的,因为之前花了很大功夫复制的之后可能会被很快换出去(因为自己不需要)。写时拷贝本质就是先共享用来读,写的时候因为各个进程(除了子线程)之间有区分,所以需要完成拷贝过程避免出错。在一些情况下:比如fork()完成之后,一般会直接使用execve系统调用完成地址空间中程序映射的替换,这样的话是无需在之前拷贝父进程的程序代码的。

2.进程执行(创建之后接着完成的)

进程刚刚创建完成时相当于是父进程的一个复制,两者执行相同的代码(两者同时指向一个mm_struct,所以拥有相同的代码区映射,所以执行相同的程序)。所以之后需要通过系统调用execve来完成子进程运行程序替换的过程,步骤如下:

1.打开关联的可执行文件->2.初始化用于加载文件内容所需要的linux_binprm结构体,在其中会初始化一份新的mm_struct(写时拷贝了)给进程使用->3.读取文件inode,并根据其中的权限等控制完成读写,收集参数及环境变量。

总之就是将关联的代码文件装载到进程上,并修改mm_struct。

总结

内核线程,用户进程,用户线程的创建均需要调用do_fork函数,不同之处在于传入的clone_flags不同。

进程有自己的独立地址空间(可以理解为有自己的页表和地址逻辑构成(mm_struct)),用户线程和父共享地址空间。用户进程和线程均有内核栈(陷入内核态要用),内核进程只有内核栈(线性映射,所以所有内核共享内核页表和栈空间)。

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值