从整体上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换

学号最后三位编号:008
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

阅读理解task_struct数据结构

Linux进程描述符也被称为进程控制块PCB,进程描述符都是task_struct类型的数据结构,它的字段包含了一个与进程相关的所有信息,不仅包含了很多进程属性的字段,而且一些字段还包括了指向其它数据结构的指针。

task_struct结构中主要包含以下内容:

  • 状态信息:如就绪、执行等状态
  • 链接信息:用来描述进程之间的家庭关系,例如指向父进程、子进程、兄弟进程等PCB的指针
  • 各种标识符:如进程标识符、用户及组标识符等
  • 时间和定时器信息:进程使用CPU时间的统计等
  • 调度信息:调度策略、进程优先级、剩余时间片大小等
  • 处理机环境信息:处理器的各种寄存器以及堆栈情况等
  • 虚拟内存信息:描述每个进程所拥有的地址空间
  • 文件系统信息:记录进程使用文件的情况

task_struct结构中部分成员变量如下所示:

struct task_struct
{
 volatile long state;	// 描述进程状态
 pid_t pid; 	// 进程标识符(PID) 
 void *stack;   // 进程内核栈
 unsigned int policy;   // 描述进程调度策略
 struct list_head tasks;   // 用于创建进程链表
 ......
};
分析fork函数对应的内核处理过程do_fork

do_fork()函数负责处理clone()、fork()和vfork()系统调用,函数原型如下所示:

long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr)

参数说明如下所示:

clone_flags通过clone标志有选择的对父进程的资源进行复制,fork,vfork和clone系统调用就是根据flag标志不同加以区分的
stack_start子进程用户态堆栈的地址
stack_size未被使用,通常被赋值为0
parent_tidptr父进程在用户态下pid的地址,只有在CLONE_PARENT_SETTID标志被设定时才有意义
child_tidptr子进程在用户态下pid的地址,也是在CLONE_CHILD_SETTID标志被设定时有意义

do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其它内核数据结构。
do_fork()函数的源代码如下所示:

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p;	// 定义一个task_struct类型的指针p
	int trace = 0; 
	long nr;
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;
		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}
	/*复制进程描述符,copy_process()的返回值是一个 task_struct 类型的指针*/
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);
	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;
		trace_sched_process_fork(current, p);
		/*得到新创建的进程的pid信息*/
		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);
		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);
		/*如果调用的 vfork()方法,初始化 vfork 完成处理信息*/
		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);   // 初始化完成变量
			get_task_struct(p);
		}
		/*将子进程加入到调度器中,为其分配 CPU,准备执行 */
		wake_up_new_task(p);
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);
		/*如果是 vfork,将父进程加入至等待队列,等待子进程完成*/
		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}
		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;	// 返回子进程的进程描述符
}

copy_process()函数的部分源代码如下所示:

static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
	int retval;
	struct task_struct *p;	// 定义一个task_struct类型的指针p
	//检查clone_flags标志的设置是否正确
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);
	if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
		return ERR_PTR(-EINVAL);
	if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
		return ERR_PTR(-EINVAL);
	if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
		return ERR_PTR(-EINVAL);
	if ((clone_flags & CLONE_PARENT) &&
				current->signal->flags & SIGNAL_UNKILLABLE)
		return ERR_PTR(-EINVAL);
	if (clone_flags & CLONE_SIGHAND) {
		if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
		    (task_active_pid_ns(current) !=
				current->nsproxy->pid_ns_for_children))
			return ERR_PTR(-EINVAL);
	}
	// 复制当前的 task_struct,此时父子进程的内容完全一样, 接下来就是重新设置子进程的一些值
	p = dup_task_struct(current);
	 //初始化互斥变量
	rt_mutex_init_task(p);
	retval = copy_creds(p, clone_flags);
	if (retval < 0)
		goto bad_fork_free;
	//检查进程数是否超过 max_threads 
	if (nr_threads >= max_threads)
		goto bad_fork_cleanup_count;
	//  初始化自旋锁
	spin_lock_init(&p->alloc_lock);
	//  初始化挂起信号
	init_sigpending(&p->pending);
	//  初始化 CPU 定时器
	p->utime = p->stime = p->gtime = 0;
	p->utimescaled = p->stimescaled = 0;
	//初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
	retval = sched_fork(clone_flags, p);
	//复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
	shm_init_task(p);
	retval = copy_semundo(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_audit;
	retval = copy_files(clone_flags, p); //文件描述符
	if (retval)
		goto bad_fork_cleanup_semundo;
	retval = copy_fs(clone_flags, p);  //进程当前工作目录信息
	if (retval)
		goto bad_fork_cleanup_files;
	retval = copy_sighand(clone_flags, p);	//信号处理表
	if (retval)
		goto bad_fork_cleanup_fs;
	retval = copy_signal(clone_flags, p);	//信号值的处理
	if (retval)
		goto bad_fork_cleanup_sighand;
	retval = copy_mm(clone_flags, p);	//内存描述符
	if (retval)
		goto bad_fork_cleanup_signal;
	retval = copy_namespaces(clone_flags, p);	//命名空间
	if (retval)
		goto bad_fork_cleanup_mm;
	retval = copy_io(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_namespaces;
	//初始化子进程内核栈
	retval = copy_thread(clone_flags, stack_start, stack_size, p);
	//为新进程分配新的pid
	if (pid != &init_struct_pid) {
		retval = -ENOMEM;
		pid = alloc_pid(p->nsproxy->pid_ns_for_children);
		if (!pid)
			goto bad_fork_cleanup_io;
	}
	//设置子进程的pid 
	p->pid = pid_nr(pid);
	//调用fork的进程为其父进程 
	if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
		p->real_parent = current->real_parent;
		p->parent_exec_id = current->parent_exec_id;
	} else {
		p->real_parent = current;
		p->parent_exec_id = current->self_exec_id;
	}
	spin_lock(&current->sighand->siglock);
	return p;
}

dup_task_struct()函数的部分源代码如下所示:

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
	struct task_struct *tsk;
	struct thread_info *ti;
	int node = tsk_fork_get_node(orig);
	int err;
	tsk = alloc_task_struct_node(node);	//申请进程描述符
	if (!tsk)
		return NULL;
	ti = alloc_thread_info_node(tsk, node);	// 申请线程描述符
	if (!ti)
		goto free_tsk;
	//将父进程的进程描述符,线程描述符,内核栈的值赋值给子进程
	err = arch_dup_task_struct(tsk, orig);	
	if (err)
		goto free_ti;
	tsk->stack = ti;	 //子进程的进程描述符的stack指针设为子进程的线程描述符
	return tsk;
free_ti:
	free_thread_info(ti);
free_tsk:
	free_task_struct(tsk);
	return NULL;
}
使用gdb跟踪分析一个fork系统调用内核处理函数do_fork

本次实验是基于实验楼中现有的实验环境进行的。
进入menu文件夹,编辑test.c文件:

cd ~/LinuxKernel/menu/
sudo vim test.c 

给qemu增加一个使用fork系统调用的菜单命令,如下所示:
在这里插入图片描述
在这里插入图片描述
在menu目录下执行如下命令:make rootfs启动MenuOS,结果如下所示:
在这里插入图片描述
使用GDB进行跟踪调试,设置如下断点:
在这里插入图片描述
在MenuOS中输入fork菜单命令以后,后面的断点依次如图所示:
首先停在sys_clone位置处:
在这里插入图片描述
然后进入do_fork中:
在这里插入图片描述
接着进入copy_process中:
在这里插入图片描述
接着进入copy_thread中:
在这里插入图片描述
最后进入ret_from_fork中:
在这里插入图片描述

  • Linux内核通过复制父进程来创建一个新进程,调用do_fork为每个新创建的进程动态地分配一个task_struct结构。copy_thread()函数中的代码p->thread.ip = (unsigned long) ret_from_fork;将子进程的 ip 设置为 ret_form_fork 的首地址,所以fork系统调用产生的子进程在系统调用处理过程中从ret_from_fork处开始执行。
  • copy_thread()函数中的代码*childregs = *current_pt_regs();将父进程的regs参数赋值到子进程的内核堆栈,里面存放了SAVE ALL中压入栈的参数,之后的RESTORE_ALL宏定义会恢复保存到堆栈中的寄存器的值。
  • fork系统调用发生一次,但是返回两次。父进程中返回值是子进程的进程号,子进程中返回值为0,可以通过返回值来判断当前进程是父进程还是子进程。
  • 整个fork系统调用的执行流程如下:
    fork->sys_clone->do_fork->copy_process->dup_task_struct->copy_thread->ret_from_fork
理解编译链接的过程和ELF可执行文件格式

程序的编译链接过程需要经历如下步骤:
在这里插入图片描述
ELF文件的全称是Executable and Linkable Format,意为可执行的、可连接的格式。

ELF文件大致可以分为如下三类:

  • 可重定位文件,保存代码和适当的数据,和其他的共享文件一起创建一个可执行文件或者一个共享文件。
  • 可执行文件,保存一个可执行的程序,该文件指出exec(BA_OS)如何创建程序进程映像。
  • 共享文件,保存代码和合适的数据,被链接编辑器(静态链接)和动态链接器进行链接。

ELF可执行文件格式的具体分析可以参考如下链接

编程使用exec*库函数加载一个可执行文件
函数原型int execve(const char *path,const char *argv[],const char *envp[])
头文件#include <unistd.h >
参数说明path:可执行文件的路径名 argv:命令行参数 envp:环境变量
返回值成功则不返回,失败则返回-1

编辑如下文件test.c

#include <unistd.h>
void main()
{
	char *argv[]={"ls","-al","/etc/passwd",(char *)0 };
	char *envp[]={"PATH=/bin",0};
	execve("/bin/ls",argv,envp);
}

运行可执行文件test,如下所示:

gcc test.c -o test
 ./test 

在这里插入图片描述
动态链接分为可执行程序装载时动态链接和运行时动态链接。

使用gdb跟踪分析一个execve系统调用内核处理函数do_execve

在实验楼提供的环境中,给qemu增加一个使用execve系统调用的菜单命令,如下所示:
在这里插入图片描述
在这里插入图片描述
在menu目录下执行如下命令:make rootfs启动MenuOS,结果如下所示:
在这里插入图片描述
使用GDB进行跟踪调试,设置如下断点:
在这里插入图片描述
在MenuOS中输入execve菜单命令以后,截图如下所示:
在这里插入图片描述
在这里插入图片描述
do_execve函数源代码如下所示:

int do_execve(struct filename *filename,const char __user *const __user *__argv,const char __user *const __user *__envp)
{
	struct user_arg_ptr argv = { .ptr.native = __argv };
	struct user_arg_ptr envp = { .ptr.native = __envp };
	return do_execve_common(filename, argv, envp);		// 此处调用do_execve_common
}
  • 装载和启动一个可执行程序的大致流程如下所示:
    sys_execve -> do_execve-> do_execve_common-> exec_binprm-> search_binary_handler -> load_elf_binary-> start_thread
  • 对于静态链接的可执行文件,eip指向该文件的文件头e_entry所指的入口地址;对于动态链接的可执行文件,eip指向动态链接器。执行静态链接程序时,execve系统调用修改内核堆栈中保存的eip的值作为新的进程的起点。
  • 新的可执行程序修改内核堆栈eip为新程序的起点,从new_ip开始执行,start_thread把返回到用户态的位置从int 0x80的下一条指令变成新的可执行文件的入口地址。
  • 执行execve系统调用时,调用execve的可执行程序陷入内核态,使用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的起点(main函数),故新的可执行程序能够顺利执行。
理解Linux系统中进程调度的时机
  • 中断处理过程(时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,内核线程作为一类的特殊的进程既可以进行主动调度,也可以进行被动调度;
  • 用户态进程无法实现主动调度,只能够通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
使用gdb跟踪分析一个schedule()函数

在实验楼提供的环境中,设置断点如下所示:
在这里插入图片描述
在这里插入图片描述
schedule()函数用于实现进程调度,它的任务是从运行队列的链表中找到一个进程,并且随后将CPU分配给这个进程。

从本质上来说,每个进程切换分为两步:

  • 切换页全局目录以安装一个新的地址空间;
  • 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU寄存器。

函数和宏的调用关系如下所示:

  • schedule() --> context_switch() --> switch_to --> __switch_to()
  • 其中__switch_to()函数主要完成硬件上下文切换,switch_to宏主要完成内核堆栈切换。
分析switch_to中的汇编代码
#define switch_to(prev, next, last)        //  prev指向当前进程,next指向被调度的进程                             
do {                                                                    
        unsigned long ebx, ecx, edx, esi, edi;                                                                                           
        asm volatile("pushfl\n\t"               /* 将标志位压栈 */     
                     "pushl %%ebp\n\t"          /* 将当前ebp压栈 */     
                     "movl %%esp,%[prev_sp]\n\t"        /* 保存当前进程的堆栈栈顶*/ 
                     "movl %[next_sp],%%esp\n\t"        /* 将下一个进程的堆栈栈顶保存到esp寄存器,完成内核堆栈的切换*/ 
                     "movl $1f,%[prev_ip]\n\t"  /* 保存当前进程的eip*/     
                     "pushl %[next_ip]\n\t"     /*将下一个进程的eip压栈 */     
                     "jmp __switch_to\n"        /*jmp通过后面的寄存器eax、edx来传递参数,__switch_to()函数通过return把next_ip弹出来 */     
                     "1:\t"                                             
                     "popl %%ebp\n\t"           /*恢复当前堆栈的ebp*/     
                     "popfl\n"                  /* 恢复当前堆栈的寄存器标志位*/     
                                                                        
                     /* output parameters */                            
                     : [prev_sp] "=m" (prev->thread.sp),              // 当前内核堆栈的栈顶
                       [prev_ip] "=m" (prev->thread.ip),             // 当前进程的eip   
                       "=a" (last),                                     
                                                                        
                       /* clobbered output registers: */                
                       "=b" (ebx), "=c" (ecx), "=d" (edx),              
                       "=S" (esi), "=D" (edi)                           
                                                                        
                       /* input parameters: */                          
                     : [next_sp]  "m" (next->thread.sp),                // 下一个进程的内核堆栈的栈顶
                       [next_ip]  "m" (next->thread.ip),                // 下一个进程的eip
                                                                        
                       /* regparm parameters for __switch_to(): */      
                       [prev]     "a" (prev),                           // 寄存器的传递
                       [next]     "d" (next));                          

						__switch_canary_iparam                
						                                    
								 : /* reloaded segment registers */           
								 "memory"); 
} while (0)

由上述分析可知,switch_to宏的执行完成了进程切换的第二步,主要完成了内核堆栈的切换。

进程上下文切换与中断上下文切换的关系

  • 对于用户态进程:用户态进程->中断上下文切换->进程进入内核态->需要切换进程->内核态调用schedule()函数,完成进程上下文的切换->新进程处于内核态中->中断上下文切换->新进程由内核态返回用户态。
  • 对于内核态线程:内核态线程主动调用schedule()函数,只有进程上下文的切换,没有发生中断上下文的切换。
总结
  • 进程上下文是可执行程序代码,是进程的重要组成部分, 是进程执行活动全过程的静态描述, 可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值以及当时的环境等。
  • Linux的进程调度基于分时技术和进程的优先级,内核通过调用schedule()函数来实现进程调度,其中context_switch宏用于完成进程上下文切换,它通过调用switch_to宏来实现关键上下文切换。
  • 在Linux中,内核线程是只有内核态没有用户态的特殊进程,内核线程可以进行主动调度,主动调度时不需要发生中断上下文的切换。
  • Linux系统的一般执行过程可以抽象成正在运行的用户态进程X切换到运行用户态进程Y的过程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值