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, &regs, 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, &regs, 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, &regs, 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 */	\
		     "mo
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值