从整理上理解进程创建可执行文件的加载和进程执行进程切换,重点理解分析forkexecve和进程切换
Posted zhongweics
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从整理上理解进程创建可执行文件的加载和进程执行进程切换,重点理解分析forkexecve和进程切换相关的知识,希望对你有一定的参考价值。
一、首先我们来看看进程控制块PCB也就是task_struct,(源码)
选出task_struct中几个关键的参数进行分析
struct task_struct {
volatile long state; //进程状态 /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; //进程内核堆栈 atomic_t usage; unsigned int flags; //进程标识符 /* per process flags, defined below */
....
unsigned int ptrace; struct list_head tasks; //进程链表 struct thread_struct thread; //在进行进程切换时与CPU状态相关的代码结构体 /* filesystem information */ struct fs_struct *fs; //文件系统相关数据结构 /* open file information */ struct files_struct *files; //文件描述列表 /* namespaces */ struct nsproxy *nsproxy; /* signal handlers */ struct signal_struct *signal; struct sighand_struct *sighand; //信号处理相关代码 .... }
二、进程的创建
fork()允许用户态下创建新的进程, fork 创造的子进程复制了父亲进程的资源,包括内存的内容task_struct内容,新旧进程使用同一代码段,复制数据段和堆栈段,这里的复制采用了注明的copy_on_write技术,即一旦子进程开始运行,则新旧进程的地址空间已经分开,两者运行独立.
在 Linux 内核中,供用户创建进程的系统调用fork()函数的响应函数是 sys_fork()、sys_clone()、sys_vfork()。这三个函数都是通过调用内核函数 do_fork() 来实现的。根据
调用时所使用的 clone_flags 参数不同,do_fork() 函数完成的工作也各异。下面结合实验过程简要分析do_fork()是怎么工作的,首先是代码以及简析如下:
1 long do_fork(unsigned long clone_flags, 2 unsigned long stack_start, 3 unsigned long stack_size, 4 int __user *parent_tidptr, 5 int __user *child_tidptr) 6 { 7 struct task_struct *p; 8 int trace = 0; 9 long nr; 10 11 // ... 12 13 // 复制进程描述符,返回创建的task_struct的指针 14 p = copy_process(clone_flags, stack_start, stack_size, 15 child_tidptr, NULL, trace); //复制子进程 16 17 if (!IS_ERR(p)) { 18 struct completion vfork; 19 struct pid *pid; 20 21 trace_sched_process_fork(current, p); 22 23 // 取出task结构体内的pid 24 pid = get_task_pid(p, PIDTYPE_PID); 25 nr = pid_vnr(pid); 26 27 if (clone_flags & CLONE_PARENT_SETTID) 28 put_user(nr, parent_tidptr); 29 30 // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 31 if (clone_flags & CLONE_VFORK) { 32 p->vfork_done = &vfork; 33 init_completion(&vfork); 34 get_task_struct(p); 35 } 36 37 // 将子进程添加到调度器的队列,使得子进程有机会获得CPU 38 wake_up_new_task(p); 39 40 // ... 41 42 // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间 43 // 保证子进程优先于父进程运行 44 if (clone_flags & CLONE_VFORK) { 45 if (!wait_for_vfork_done(p, &vfork)) 46 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); 47 } 48 49 put_pid(pid); 50 } else { 51 nr = PTR_ERR(p); 52 } 53 return nr; 54 }
copy_process()主要完成进程数据结构,各种资源的初始化。
里面包含了两个主要的函数dup_task_struct () 和 copy_thread()
对于dup_task_struct():
tsk = alloc_task_struct_node(node);为task_struct开辟内存
ti = alloc_thread_info_node(tsk, node);ti指向thread_info的首地址,同时也是系统为新进程分配的两个连续页面的首地址。
err = arch_dup_task_struct(tsk, orig);复制父进程的task_struct信息到新的task_struct里, (dst = src;)
tsk->stack = ti;task的对应栈
setup_thread_stack(tsk, orig);初始化thread info结构
set_task_stack_end_magic(tsk);栈结束的地址设置数据为栈结束标示(for overflow detectio
但是创建新进程从哪里开始呢?
kernel中是可以指定新进程开始的位置(也就是通过eip寄存器指定代码行)。fork中也有相似的机制 这涉及子进程的内核堆栈数据状态和task_ struct中thread记录的sp和ip的一致性问题,这是在copy_ thread in copy_ process设定的。
让我们在来看一下copy_thread函数:
132int copy_thread(unsigned long clone_flags, unsigned long sp, 133 unsigned long arg, struct task_struct *p) 134{ 135 struct pt_regs *childregs = task_pt_regs(p); 136 struct task_struct *tsk; 137 int err; 138 139 p->thread.sp = (unsigned long) childregs; 140 p->thread.sp0 = (unsigned long) (childregs+1); 141 memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps)); 142 143 if (unlikely(p->flags & PF_KTHREAD)) { 144 /* kernel thread */ 145 memset(childregs, 0, sizeof(struct pt_regs)); 146 p->thread.ip = (unsigned long) ret_from_kernel_thread; 147 task_user_gs(p) = __KERNEL_STACK_CANARY; 148 childregs->ds = __USER_DS; 149 childregs->es = __USER_DS; 150 childregs->fs = __KERNEL_PERCPU; 151 childregs->bx = sp; /* function */ 152 childregs->bp = arg; 153 childregs->orig_ax = -1; 154 childregs->cs = __KERNEL_CS | get_kernel_rpl(); 155 childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED; 156 p->thread.io_bitmap_ptr = NULL; 157 return 0; 158 } 159 *childregs = *current_pt_regs(); 160 childregs->ax = 0; 161 if (sp) 162 childregs->sp = sp; 163 164 p->thread.ip = (unsigned long) ret_from_fork; 165 task_user_gs(p) = get_user_gs(current_pt_regs()); 166 167 p->thread.io_bitmap_ptr = NULL; 168 tsk = current; 169 err = -ENOMEM; 170 171 if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) { 172 p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr, 173 IO_BITMAP_BYTES, GFP_KERNEL); 174 if (!p->thread.io_bitmap_ptr) { 175 p->thread.io_bitmap_max = 0; 176 return -ENOMEM; 177 } 178 set_tsk_thread_flag(p, TIF_IO_BITMAP); 179 } 180 181 err = 0; 182 183 /* 184 * Set a new TLS for the child thread? 185 */ 186 if (clone_flags & CLONE_SETTLS) 187 err = do_set_thread_area(p, -1, 188 (struct user_desc __user *)childregs->si, 0); 189 190 if (err && p->thread.io_bitmap_ptr) { 191 kfree(p->thread.io_bitmap_ptr); 192 p->thread.io_bitmap_max = 0; 193 } 194 return err; 195}
copy_thread的流程如下:
(1) 获取子进程寄存器信息的存放位置
(2) 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
(3)如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出 。
(4)将父进程的寄存器信息复制给子进程。
(5)将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0。
(6)子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。 从上面的流程中,我们看出,子进程复制了父进程的上下文信息,仅仅对某些地方做了改动,运行逻辑和父进程完全一致
*childregs = *current_pt_regs(); 复制内核堆栈(复制的pt_regs,是SAVE_ALL中系统调用压栈的那一部分。) childregs->ax = 0; 子进程的fork返回0 p->thread.sp = (unsigned long) childregs; 调度到子进程时的内核栈顶 p->thread.ip = (unsigned long) ret_from_fork; 调度到子进程时的第一条指令地址 ip指向的是ret_from_fork,所以是从这里开始执行的。
三、实验过程
四、可执行文件的加载
1.编译链接的过程
C代码(.c)--》经过编译器预处理,编译成汇编代码(.asm) --》 汇编器,生成目标代码(.o) ---》链接器,链接成可执行文件(.out) ---》OS将可执行文件加载到内存里执行
1. 预处理
gcc -E -o hello.cpp hello.c -m32 预处理(文本文件) 预处理负责把include的文件包含进来及宏替换等工作
2. 编译
gcc -x cpp-output -S -o hello.s hello.cpp -m32 编译成汇编代码(文本文件)
3. 汇编
gcc -x assembler -c hello.s -o hello.o -m32 汇编成目标代码(ELF格式,二进制文件,有一些机器指令,只是还不能运行)
4. 链接
gcc -o hello hello.o -m32 链接成可执行文件(ELF格式,二进制文件)
在hello可执行文件里面使用了共享库,会调用printf,libc库里的函数
gcc -o hello.static hello.o -m32 -static 静态链接
把执行所需要依赖的东西都放在程序内部
2.ELF三种主要的目标文件:
1.可重定位:保存代码和适当数据,用来和其他的object文件一起创建可执行/共享文件,主要是.o文件
2.可执行文件:指出了exec如何创建程序进程映像,怎么加载,从哪里开始执行
3.共享object文件:保存代码和适当数据,用来被下面的两个连接器链接。
(1)连接editor,连接可重定位、共享object文件。即装载时链接。
(2)动态链接器,联合可执行、其他共享object文件创建进程映像。即运行时链接
3.可执行程序的执行环境
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
$ ls -l /usr/bin 列出/usr/bin下的目录信息
Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
例如,int main(int argc, char *argv[])
又如, int main(int argc, char *argv[], char *envp[])
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
库函数exec*都是execve的封装例程
4.可执行程序的装载
Linux 一般通过shell程序为执行环境来启动一个可执行程序。Shell本身不限制命令行参数的个数,它受限于命令自身;Shell会调用一个系统调用exece将命令参数和环境参数传递给可执行程序的main函数。
命令行参数与环境变量的保存与传递:
当fork一个子进程时,是先复制父进程,再调用exece,会把原来的进程环境覆盖掉,用户态堆栈也被清空。用户态堆栈以start_stack作为main函数的起点,把argv[ ]命令行参数 和envp[ ]环境变量的内容通过指针的方式传递到系统调用exeve(内核处理函数);exeve创建一个新的用户态堆栈时把上面的命令行参数与环境变量拷贝到新的用户态堆栈里,从而初始化新的可执行程序的上下文环境。exece在内核态下装载可执行程序,再返回用户态。所以它先进行函数调用参数传递,然后系统调用参数传递,最后又进行函数调用参数传递
1549int do_execve(struct filename *filename, 1550 const char __user *const __user *__argv, 1551 const char __user *const __user *__envp) 1552{ 1553 struct user_arg_ptr argv = { .ptr.native = __argv }; 1554 struct user_arg_ptr envp = { .ptr.native = __envp }; 1555 return do_execve_common(filename, argv, envp); 1556}
1430static int do_execve_common(struct filename *filename, 1431 struct user_arg_ptr argv, 1432 struct user_arg_ptr envp) 1433{ 1434 struct linux_binprm *bprm; 1435 struct file *file; 1436 struct files_struct *displaced; 1437 int retval; 1438 1439 if (IS_ERR(filename)) 1440 return PTR_ERR(filename); 1441 1442 /* 1443 * We move the actual failure in case of RLIMIT_NPROC excess from 1444 * set*uid() to execve() because too many poorly written programs 1445 * don‘t check setuid() return code. Here we additionally recheck 1446 * whether NPROC limit is still exceeded. 1447 */ 1448 if ((current->flags & PF_NPROC_EXCEEDED) && 1449 atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) { 1450 retval = -EAGAIN; 1451 goto out_ret; 1452 } 1453 1454 /* We‘re below the limit (still or again), so we don‘t want to make 1455 * further execve() calls fail. */ 1456 current->flags &= ~PF_NPROC_EXCEEDED; 1457 1458 retval = unshare_files(&displaced); 1459 if (retval) 1460 goto out_ret; 1461 1462 retval = -ENOMEM; 1463 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); 1464 if (!bprm) 1465 goto out_files; 1466 1467 retval = prepare_bprm_creds(bprm); 1468 if (retval) 1469 goto out_free; 1470 1471 check_unsafe_exec(bprm); 1472 current->in_execve = 1; 1473 1474 file = do_open_exec(filename); 1475 retval = PTR_ERR(file); 1476 if (IS_ERR(file)) 1477 goto out_unmark; 1478 1479 sched_exec(); 1480 1481 bprm->file = file; 1482 bprm->filename = bprm->interp = filename->name; 1483 1484 retval = bprm_mm_init(bprm); 1485 if (retval) 1486 goto out_unmark; 1487 1488 bprm->argc = count(argv, MAX_ARG_STRINGS); 1489 if ((retval = bprm->argc) < 0) 1490 goto out; 1491 1492 bprm->envc = count(envp, MAX_ARG_STRINGS); 1493 if ((retval = bprm->envc) < 0) 1494 goto out; 1495 1496 retval = prepare_binprm(bprm); 1497 if (retval < 0) 1498 goto out; 1499 1500 retval = copy_strings_kernel(1, &bprm->filename, bprm); 1501 if (retval < 0) 1502 goto out; 1503 1504 bprm->exec = bprm->p; 1505 retval = copy_strings(bprm->envc, envp, bprm); 1506 if (retval < 0) 1507 goto out; 1508 1509 retval = copy_strings(bprm->argc, argv, bprm); 1510 if (retval < 0) 1511 goto out; 1512 1513 retval = exec_binprm(bprm); 1514 if (retval < 0) 1515 goto out; 1516 1517 /* execve succeeded */ 1518 current->fs->in_exec = 0; 1519 current->in_execve = 0; 1520 acct_update_integrals(current); 1521 task_numa_free(current); 1522 free_bprm(bprm); 1523 putname(filename); 1524 if (displaced) 1525 put_files_struct(displaced); 1526 return retval; 1527 1528out: 1529 if (bprm->mm) { 1530 acct_arg_size(bprm, 0); 1531 mmput(bprm->mm); 1532 } 1533 1534out_unmark: 1535 current->fs->in_exec = 0; 1536 current->in_execve = 0; 1537 1538out_free: 1539 free_bprm(bprm); 1540 1541out_files: 1542 if (displaced) 1543 reset_files_struct(displaced); 1544out_ret: 1545 putname(filename); 1546 return retval; 1547}
整体调用关系为sys_execve()->do_execve()->do_execveat_common()->__do_execve_file()->prepare_binprm()->search_binary_handler()->load_elf_binary()->start_thread().
五、实验过程
七、进程调度的时机
发生方式:
1 中断处理过程(时钟中断、I/O中断、系统调用和异常)中或返回用户态根据need_resched标记调用
2 内核线程直接调用进行进程切换,也可以在中断处理过程中进行调度,内核线程可以主动调度,也可以被动调度
3 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,在中断处理过程中调度
调度中介:schedule()函数
1 schedule()函数开始,禁用内核抢占并初始化一些局部变量,此时,schedule()检查运行队列中剩余的可运行进程数,如果有可调用进程,开始调用。
2 schedule()函数中在switch_to宏之后紧接着的指令并不由next进程立即执行,而是稍后当调度程序选择prev又执行时由prev执行。 prev局部变量并不指向开始描述schedule()时所替换出去的原来的那个进程,而是指向prev被调度时由prev替换出的原来那个进程。
3 紧接着context_switch()函数调用之后,宏barrier()产生一个代码优化屏障。执行finish_task_switch()函数。
4 schedule()函数最后执行的是:重新获得大内核锁,重新启动内核抢占,并检查是否其他进程设置了当前进程的TIF_RESCHED标志,如果是,整个schedule()函数重新开始执行
参考链接 https://www.cnblogs.com/chuishi/p/5400943.html
进程调度函数关系:
schedule() --> context_switch() --> switch_to --> __switch_to()
switch_to 完成进程切换,让我们看一下关键代码
31#define switch_to(prev, next, last) 32do { 33 /* 34 * Context-switching clobbers all registers, so we clobber 35 * them explicitly, via unused output variables. 36 * (EAX and EBP is not listed because EBP is saved/restored 37 * explicitly for wchan access and EAX is the return value of 38 * __switch_to()) 39 */ 40 unsigned long ebx, ecx, edx, esi, edi; 41 42 asm volatile("pushfl " /* save flags */ 43 "pushl %%ebp " /* save EBP */ 44 "movl %%esp,%[prev_sp] " /* save ESP */ 45 "movl %[next_sp],%%esp " /* restore ESP */ 46 "movl $1f,%[prev_ip] " /* save EIP */ "保存当前进程的eip,恢复的时候从prev_ip来恢复eip 47 "pushl %[next_ip] " /* restore EIP */ 把下个进程的起点ip位置压到next进程堆栈栈顶(起点) 48 __switch_canary 49 "jmp __switch_to " /* regparm call */ jmp通过prev和next寄存器的方式传递参数 50 "1: " 51 "popl %%ebp " /* restore EBP */ 恢复上下文,next曾经push过ebp 52 "popfl " /* restore flags */ 53 54 /* output parameters */ 55 : [prev_sp] "=m" (prev->thread.sp), 56 [prev_ip] "=m" (prev->thread.ip), prev_sp是内核堆栈栈顶 prev_ip是当前进程的eip */ 57 "=a" (last), 58 59 /* clobbered output registers: */ 60 "=b" (ebx), "=c" (ecx), "=d" (edx), 61 "=S" (esi), "=D" (edi) 62 63 __switch_canary_oparam 64 65 /* input parameters: */ 66 : [next_sp] "m" (next->thread.sp), ext_sp下一个进程的内核堆栈的栈顶 67 [next_ip] "m" (next->thread.ip), ext_ip下一个进程执行的起点,一般是$1f,对于新创建的子进程是ret_from_fork*/ 68 69 /* regparm parameters for __switch_to(): */ 70 [prev] "a" (prev), 71 [next] "d" (next) 72 73 __switch_canary_iparam 74 75 : /* reloaded segment registers */ 76 "memory"); 77}
分析见汇编代码注释
八、实验过程