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

 

327

原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

阅读并理解 task_struct 数据结构

代码位于 http://codelab.shiyanlou.com/source/xref/linux-3.18.6/include/linux/sched.h#1235

该结构体定义了 Linux 操作系统中的 PCB

其中最重要的项有:

  • state:描述进程状态,-1 表示不可运行,0 表示可运行,大于 0 表示停止运行
  • stack:描述进程栈
  • usage:描述进程是否被使用
  • flags:描述进程的标志
  • prio,static_prio,normal_prio:描述进程优先级
  • sched 相关结构体:描述进程调度相关数据
  • policy:描述进程策略
  • cpu 相关结构体:描述进程的 cpu 相关数据
  • tasks,children 等:存储所有当前进程的子进程
  • mm,active_mm,vmacache:描述进程内存分配与缓存相关数据
  • exit_state,exit_code,exit_signal 等:描述进程退出状态
  • pid:描述进程 id
  • real_parent,parent:描述父进程 PCB
  • start_time,real_start_time 等 time 相关数据:描述进程运行时间
  • fs,files:描述进程文件系统信息与打开的文件相关信息
  • signal,blocked 等数据:描述进程调度状态信息
  • lock 等数据:描述进程加锁状态信息

通过阅读该数据结构,可以更深刻的体会到操作系统相关知识在具体实践中的应用

分析 fork 函数对应的内核处理过程 do_fork

代码位于 http://codelab.shiyanlou.com/source/xref/linux-3.18.6/kernel/fork.c#1623

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;
    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;
    }
     
    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 = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);
 
        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, parent_tidptr);
 
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }
 
        wake_up_new_task(p);

 	if (unlikely(trace))
	    ptrace_event_pid(trace, pid);

        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;
}

  

首先根据标志位 clone_flags 判断是否要对该函数进行追踪

然后复制当前进程结构体,指针 p 指向新创建的进程结构体

判断 p 是否存在,若不存在,返回错误 nr = PTR_ERR(p)

若存在,则进一步执行 fork 处理

先从新进程结构体中取出进程 pid,并将其设置为返回值 nr

接着根据 clone_flags 设置父进程 id

然后判断否是 vfork,若不是,不做处理;若是,要确保父进程在子进程完成初始化后才能运行

将子进程加入调度队列

处理 vfork 调用中父进程等待子进程,确保子进程先运行

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

使用 MenuOS 中的 test_fork.c 代替 test.c 启动 MenuOS

其中 Fork 测试函数如下

int Fork(int argc, char *argv[])
{
	int pid;
	/* fork another process */
	pid = fork();
	if (pid<0) 
	{ 
		/* error occurred */
		fprintf(stderr,"Fork Failed!");
		exit(-1);
	} 
	else if (pid==0) 
	{
		/*	 child process 	*/
    	printf("This is Child Process!\n");
	} 
	else 
	{ 	
		/* 	parent process	 */
    	printf("This is Parent Process!\n");
		/* parent will wait for the child to complete*/
		wait(NULL);
		printf("Child Complete!\n");
	}
}

在 GDB 中打以下断点

 b sys_clone
 b dup_task_struct
 b do_fork
 b copy_process
 b copy_thread
 b ret_from_fork

运行结果

其中调试过程如下

观察实验结果,Fork 函数的调用过程为 sys_clone->do_fork->copy_process->dup_task_struct->copy_thread->ret_form_fork 

 理解编译链接的过程和 ELF 可执行文件格式

整个编译链接过程如下

源文件 .c/.cpp 经过预处理,编译,汇编之后生成 .o,所有的 .o 链接起来共同构成可执行文件

  预处理:主要是做一些代码文本的替换工作(该替换是一个递归逐层展开的过程)

  编译:把预处理完的文件进行一系列词法分析(lex)、语法分析(yacc)、语义分析及优化后生成汇编代码,这个过程是程序构建的核心部分

  汇编:汇编代码->机器指令

  链接:这里讲的链接,严格说应该叫静态链接。多个目标文件、库->最终的可执行文件(拼合的过程)

ELF 格式可执行文件为 Linux 可执行文件格式,包括三种主要的类型:可执行文件、可重定向文件、共享库

  一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象

  一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件

  一个共享库文件保存着代码和合适的数据,用来被不同的两个链接器链接

编程使用 exec* 库函数加载一个可执行文件

编写一个 .c 文件

#include <stdio.h>
int main()
{
     printf("Hello World!\n");
     return 0;
}

再编写一个 exec* 测试文件

#include <unistd.h>
int main() {
    char *argv[] = {"./test", "test", (char *)0};
    char *envp[] = {0};
    execve("./test", argv, envp);
    return 0;
}

将这两个文件编译成可执行文件

gcc test.c -o test

gcc exec.c -o exec

运行 exec 即可打印出 Hello World!

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

在之前的 MenuOS 的 Fork 函数中加入 execlp("/bin/ls",“ls”,NULL);

在 GDB 中打以下断点

b do_execve
b sys_execve
b do_execve_common
b exec_biniprm
b search_binary_hanlder
b load_elf_binary

运行结果

观察实验结果,调用顺序为sys_execve()->do_execve()->do_execveat_common()->__do_execve_file()->prepare_binprm()->search_binary_handler()->load_elf_binary()->start_thread()

 理解Linux系统中进程调度的时机

调用schedule()的位置:

  中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()

  内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度

  用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度

搜索 schedule() 调用文件

 

可发现在网络,驱动,文件系统,加锁,内核相关处理中多次调用 schedule 函数

使用 gdb 跟踪分析一个 schedule() 函数 

使用 gdb 打断点

b schedule
b pick_next_task
b context_switch
b __switch_to

运行结果如下

可以看出 schedule 调用 _schedule,_schedule 调用 pick_next_task,context_switch 函数,context_switch 函数调用 __switch_to

分析switch_to中的汇编代码

asm volatile("pushfl\n\t"      /* 保存当前进程的标志位 */   
         "pushl %%ebp\n\t"        /* 保存当前进程的堆栈基址EBP   */ 
         "movl %%esp,%[prev_sp]\n\t"  /* 保存当前栈顶ESP   */ 
         "movl %[next_sp],%%esp\n\t"  /* 把下一个进程的栈顶放到esp寄存器中,完成了内核堆栈的切换,从此往下压栈都是在next进程的内核堆栈中。   */ 
       

		 "movl $1f,%[prev_ip]\n\t"    /* 保存当前进程的EIP   */ 
         "pushl %[next_ip]\n\t"   /* 把下一个进程的起点EIP压入堆栈   */    
         __switch_canary                   
         "jmp __switch_to\n"  /* 因为是函数所以是jmp,通过寄存器传递参数,寄存器是prev-a,next-d,当函数执行结束ret时因为没有压栈当前eip,所以需要使用之前压栈的eip,就是pop出next_ip。  */ 


		 "1:\t"               /* 认为next进程开始执行。 */         
		 "popl %%ebp\n\t"     /* restore EBP   */    
		 "popfl\n"         /* restore flags */  
                                    
		 /* output parameters 因为处于中断上下文,在内核中
		 prev_sp是内核堆栈栈顶
		 prev_ip是当前进程的eip */                
		 : [prev_sp] "=m" (prev->thread.sp),     
		 [prev_ip] "=m" (prev->thread.ip),  //[prev_ip]是标号        
		 "=a" (last),                 
                                    
		/* clobbered output registers: */     
		 "=b" (ebx), "=c" (ecx), "=d" (edx),      
		 "=S" (esi), "=D" (edi)             
                                       
		 __switch_canary_oparam                
                                    
		 /* input parameters: 
		 next_sp下一个进程的内核堆栈的栈顶
		 next_ip下一个进程执行的起点,一般是$1f,对于新创建的子进程是ret_from_fork*/                
		 : [next_sp]  "m" (next->thread.sp),        
		 [next_ip]  "m" (next->thread.ip),       
                                        
	     /* regparm parameters for __switch_to(): */  
		 [prev]     "a" (prev),              
		 [next]     "d" (next)               
                                    
		 __switch_canary_iparam                
                                    
		 : /* reloaded segment registers */           
		 "memory");                  
} while (0)

首先在当前进程 prev 的内核栈中保存 esi,edi 及 ebp 寄存器的内容

然后将 prev 的内核堆栈指针 ebp 存入 prev->thread.esp 中

把将要运行进程 next 的内核栈指针 next->thread.esp 置入 esp 寄存器中将 popl 指令所在的地址保存在 prev->thread.eip 中

这个地址就是 prev 下一次被调度通过 jmp 指令(而不是call指令)转入一个函数 __switch_to() 恢复 next 上次被调离时推进堆栈的内容

从现在开始,next 进程就成为当前进程而真正开始执行.

总结

本次实验加深了我对Linux系统的执行过程的理解

其中 Linux 通过复制父进程来创建一个新进程,通过调用 do_fork 来实现并为每个新创建的进程动态地分配一个 task_struct 结构

fork() 函数被调用一次,但返回两次

可以通过 fork,复制一个已有的进程,进而产生一个子进程

而 schedule() 函数实现进程调度,context_ switch 完成进程上下文切换,switch_ to 完成寄存器的切换

转载于:https://www.cnblogs.com/svens/p/10604485.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值