linux进程与进程调度
进程
进程是一段拥有独立存储空间的可执行程序,每个进程在内核中有一个task_struct用来描述进程拥有的各种资源和他的状态,包括进程的存储空间mm,运行状态state,拥有的时间片counter,是否需要调度need_resched,遵循的调度策略policy,进程号pid,他的各种资源的限制rlim数组,需要处理的信号pending等等等等。
没有独立的存储空间的进程称之为线程thread,没有用户空间称之为内核线程,共享用户空间则称之为用户线程。
不管是线程还是进程他都在内核中拥有一个系统空间堆栈,一般与task_struct同时分配共2个page大小,task_struct结束的地方一直到两个页面8K结尾的地方都用做进程的系统空间堆栈。
所以内核中用来访问当前进程task_struct的宏定义CURRENT就是通过将当前堆栈的esp指针的低13位清零来实现的。
static inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
return current;
}
#define current get_current()
进程的创建
linux的0号进程idle是静态创建的,它由宏INIT_TASK静态定义了init_task的各项参数。
1号进程init进程在start_kernel中创建,它由kernel_thread通过系统调用__NR_clone,sys_clone创建。
asmlinkage void __init start_kernel(void)
{
...
sched_init();
...
kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
...
init进程是所有进程的祖先,其他进程都通过复制init进程创建。
内核提供了三个系统调用用于创建进程,可以看到,他们都是调用do_fork,只是使用的参数有所不同。
三个系统调用的本质都是分配一个2页大小的空间,然后复制当前进程的task_struct作为当前进程的子进程,区别在于根据调用do_fork时的第一个参数clone_flag的内容调整复制的内容。
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0);
}
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s, 0);
}
/*
* This is trivial, and on the face of it looks like it
* could equally well be done in user mode.
*
* Not so, for quite unobvious reasons - register pressure.
* In user mode vfork() cannot have a stack frame, and if
* done by calling the "clone()" system call directly, you
* do not have enough call-clobbered registers to hold all
* the information you need.
*/
asmlinkage int sys_vfork(struct pt_regs regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0);
}
int do_fork(unsigned long clone_flags, unsigned long stack_start,
struct pt_regs *regs, unsigned long stack_size)
sys_fork会分配自己的系统空间mm,然后复制父进程的mm中的内容,这样的子进程拥有自己的系统空间,是完全独立的。但是,在复制mm的过程中,却没有真正的分配物理页面进而复制页面的内容,而只是将父进程的页表项设置成写保护,然后将页表项设置给子进程,并将物理页面的使用计数加1,这样就在读操作的层面上共享了父子进程的物理页面。因为此时父子进程的页面都设置为写保护,所以不论父子进程发起写操作时都会引发一次页面异常,而页面异常的处理就是另行分配一个页面,将原页面的内容复制过来,并将页表项改为可写,这样就实现了父子进程的内存独立,这种方法被称为copy on write(cow)。
sys_vfork则会共享父进程的mm,只是将自己的mm指针指向父进程的mm并递增父进程mm的使用计数,这样的进程实际和父进程共用一套存储空间。
vfork的结尾父进程会调用down(&sem)进入睡眠,等待子进程退出才或者子进程已经自己创建了存储空间mm,脱离父进程,父进程才退出睡眠,继续运行。这样才能保证同时只有一个进程操作同一块存储空间导致错误。
sys_clone的复制内容由参数决定fs,files,signal,mm,thread(系统空间堆栈)。
运行
do_fork返回用户空间后,子进程可以使用系统调用execve执行一个可执行文件。
系统调用execve的内核入口为sys_execve
linux使用一个结构来描述一个可执行文件
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
unsigned long p; /* current top of mem */
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
int argc, envc;
char * filename; /* Name of binary */
unsigned long loader, exec;
};
buf存放文件的头部,page是n个页面,存放若干个参数,每个参数分配一个页面,最多有MAX_ARG_PAGES个参数,p为当前内存的顶部地址,file是文件路径,argc, envc分别为参数和环境变量的个数,filename为文件名字。
sys_execve将文件内容读取到linux_binprm结构后,将这个结构作为参数传递给search_binary_handler,遍历系统中支持的格式,由相应格式的处理函数认领,对于a.out文件而言,这个函数是,load_aout_binary
load_aout_binary首先检查linux_binprm文件中的内存大小设置,然后因为子进程要自己独立运行了,所以要先将还在与父进程共享的资源独立出来,在开辟了自己的内存空间(mm)后,将linux_binprm中的代码段数据段根据文件头当中的设置读取到相应的内存中,准备好后,调用up告诉父进程,自己已经独立,不需要父进程继续等待了。最后将参数写入进程的用户空间堆栈,再调用start_thread设置好用户空间的代码段数据段地址,并将返回地址设置为可执行文件的入口地址,这样,当进程从系统空间返回到用户空间时,就可以执行文件中的程序了。
exit和wait4
exit系统调用负责将进程设置为TASK_ZOMBIE状态,在此之前释放task_struct中占用的所有资源。然后还要将进程从亲戚关系中解脱出来,将当前进程的子进程全部托孤给init_task进程,并通知父进程回收task_struct及系统空间堆栈
wait4系统调用负责等待子进程唤醒,然后回收子进程占用的系统资源,也就是task_struct。
父进程调用wait4系统调用时,先建立一个wait结构,子进程在通知父进程之前会在父进程的thread_group中的所有线程的wait_chldexit队列,并且唤醒所有这个队列不为空的线程(将父进程的状态置为TASK_RUNNING)。
之后父进程将自己设置成TASK_INTERRUPTIBLE,然后在自己的子进程中遍历参数中的pid,找到以后若发现子进程在TASK_ZOMBIE或者TASK_STOP状态则释放子进程的资源,将自己的状态设置为可执行状态,等待系统调度,删除wait队列中的wait结构。
进程的调度与切换
linux调度策略分为三种,一种是常规的没有实时性要求的交互性进程SCHED_OTHER,二是针对实时性较强但每次运行时间比较短的进程SCHED_FIFO,三是针对每次运行时间比较长的进程SCHED_RR。
系统调度是否执行的标志为当前进程的task->need_resched,该标志位为1时,表示当前进程需要让出cpu调度给其他可执行队列中的进程。
task->need_resched在以下三种状态下会被置为1:
1、时钟中断中发现当前进程的时间片用尽(task->counter)
2、当前进程唤醒了一个比自己更有资格运行的进程
3、当前进程通过改变调度策略或主动礼让(SCHED_YIELD)时。
这三种情况当前进程仅会将自己的need_resched置1,而真正的进程切换则发生在返回用户空间的前夕
ret_with_reschedule:
cmpl $0,need_resched(%ebx)
jne reschedule
cmpl $0,sigpending(%ebx)
jne signal_return
restore_all:
RESTORE_ALL
不论是系统调用还是中断返回都会经过ret_with_reschedule,这里判断当前的need_resched是否需要调度。需要的话就跳转到reschedule进入schedule函数进行调度。
还有一种情况是进程在系统空间中主动调用schedule函数,通常实在nanosleep和pause系统调用中,sleep先将自己设置成TASK_INTERRUPTIBLE,然后设定睡眠时间,初始化一个timer并将它挂到系统的定时器列表上,然后调用schedule()在时钟中断的bh函数中发现一个timer到点则将timer所在的进程唤醒。
pause则设置完自身状态后立马调度,所以他只会被信号唤醒。
schedule主要的事情有几个:
1、由于当前进程即将进入睡眠,所以要先将悬而未决的softirq和signal处理完
2、遍历可执行队列,找到权重最高的进程
3、切换页面表
4、进程切换
1、softirq直接调用do_softirq进入软中断处理队列,而有signal带处理的时候则将当前进程重新设置为running状态,调度从当前进程开始。
2、遍历队列时,按进程的调度策略,分两种情况,一种是SCHED_OTHER,权重由counter+20-nice值决定,否则由1000+rt_priority决定。rt进程的权重一定比非rt的大,所以非rt的进程总是能优先调度。SCHED_FIFO的进程一经进入运行,就要执行到结束退出为止,除非有优先级更高的进程抢占它,SCHED_RR的进程则实行时间片轮换制,当时间片用完后,SCHED_RR进程被移置可执行队列的队尾,这样对于同优先级的进程来讲,他们实现了分时占用cpu。
当队列中所有的counter都为0时,说明当前队列中只有SCHED_OTHER的进程,并且他们的时间片都用完了,那么就统一重新分配时间片,每次重新分配都将时间片加上counter>>1,重新分配时间片针对系统中的所有进程,而不仅仅是可执行队列,这样,加上counter>>1以后,不在可执行队列中的进程在进入可执行队列中时便可得到一定量的优惠,不过由于每次都右移一位,这种优惠永远不会超过原始值的2倍。
3、切换页面表时如果发现即将运行的进程没有mm则它是一个内核线程,此时需要借用当前进程的mm,将next的active_mm指向当前进程的mm,并递增mm的使用计数即可。
如果由mm,则可以直接将mm中的页表目录地址写入cr3中。
4、切换进程的最后一步就是保存当前进程的堆栈指针当前的esp写入进程描述结构的thread.esp,然后启用新进程的esp(thread.esp)写入esp寄存器,此时堆栈已经切换到新的进程堆栈,将返回地址,也就是1标号处的地址写入老进程的thread.eip中,这样在老进程再次被调度的时候就会从1标号处开始运行。
此时将新进程的eip写入堆栈,然后jmp到__switch_to中运行,此时,新的eip就成了__switch_to的返回地址,对于新的进程来说,它运行到了上次被调离时保存在eip中的地址。
__switch_to只负责将tss的esp0指向切换后的esp地址。
#define switch_to(prev,next,last) do { \
asm volatile("pushl %%esi\n\t" \
"pushl %%edi\n\t" \
"pushl %%ebp\n\t" \
"movl %%esp,%0\n\t" /* save ESP */ \
"movl %3,%%esp\n\t" /* restore ESP */ \
"movl $1f,%1\n\t" /* save EIP */ \
"pushl %4\n\t" /* restore EIP */ \
"jmp __switch_to\n" \
"1:\t" \
"popl %%ebp\n\t" \
"popl %%edi\n\t" \
"popl %%esi\n\t" \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
"=b" (last) \
:"m" (next->thread.esp),"m" (next->thread.eip), \
"a" (prev), "d" (next), \
"b" (prev)); \
} while (0)
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' | ‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" | “Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash | – is en-dash, — is em-dash |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。1
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎