Linux内核分析——进程和程序

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

一、进程描述符

Linux 的进程描述符在 linux/include/linux/sched.h 文件中定义的名为 task_struct 的结构体(以 linux-5.0.1 内核为例,以下相同)。该结构体内容庞大繁杂,包含了一系列和进程运行相关的内容。

  1. 部分描述进程的底层信息

     #ifdef CONFIG_THREAD_INFO_IN_TASK
         struct thread_info		thread_info;
     #endif 
    
  2. 指向内存部分的指针

     void *stack;         // 内核栈指针
     struct mm_struct *mm, *active_mm; // 指向进程地址空间
     unsigned int ptrace; // 系统调用相关   
    
  3. 进程本身信息

     /* -1 unrunnable, 0 runnable, >0 stopped: 进程状态 **/
     volatile long state;
     unsigned int flags;  // 进程状态标志
     /** 进程退出 */
     int exit_state; int exit_code; int exit_signal;
     /** 进程标识号 */
     pid_t pid; pid_t tgid;
     struct pid *thread_pid;
     struct hlist_node pid_links[PIDTYPE_MAX];
    
     /** 用于通知LSM是否被do_execve()函数所调用 */
     unsigned in_execve:1;
    
     /** 在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址*/
     struct completion *vfork_done;
     /* CLONE_CHILD_SETTID: */
     int __user *set_child_tid;
     /* CLONE_CHILD_CLEARTID: */
     int __user *clear_child_tid;
    
  4. 进程调度

     /* 进程调度优先级 **/
     int prio, static_prio, normal_prio;
     unsigned int rt_priority; // 实时进程的优先级
     const struct sched_class *sched_class; // 进程调度类
     struct sched_entity se; // 普通进程调度实体
     struct sched_rt_entity rt; // 实时进程调度实体
     unsigned int policy;      // 调度策略       
    
  5. 进程链表

     struct list_head tasks, children, sibling;
     struct task_struct *group_leader;
    
  6. 文件系统

     /* Filesystem information: */
     struct fs_struct *fs;
     /* Open file information: */
     struct files_struct *files;
    
  7. 中断

     #ifdef CONFIG_TRACE_IRQFLAGS
         unsigned int irq_events;
         unsigned long hardirq_enable_ip;
         unsigned long hardirq_disable_ip;
         unsigned int hardirq_enable_event;
         unsigned int hardirq_disable_event;
         int hardirqs_enabled;
         int hardirq_context;
         unsigned long softirq_disable_ip;
         unsigned long softirq_enable_ip;
         unsigned int softirq_disable_event;
         unsigned int softirq_enable_event;
         int softirqs_enabled;
         int softirq_context;
     #endif
    

其他的还包括对进程通信,内存管理,死锁检测和设备管理等相关部分。可看到进程是操作系统基本组成部分,是资源分配、调度,设备管理的基本单位,在操作系统运行中发挥重要的作用。

二、fork 调用

1. do_fork

该函数位于 linux/kernel/fork.c 中,是 fork 系统调用进行进程创建的关键函数。函数声明如下所示:

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

2. gdb 跟踪 fork

  1. 利用 menu os 在 test.c 文件中添加使用 fork 系统调用的函数;

     int testFork(int argc, char *argv[]){
         pid_t fpid; 
         int count=0;  
         fpid=fork();   
         if (fpid < 0)   
             printf("error in fork!");   
         else if (fpid == 0) {  
             printf("i am the child process, my process id is %d\n",getpid());        
             count++;  
         }  
         else {  
             printf("i am the parent process, my process id is %d\n",getpid());   
             count++;  
         }  
         printf("result: %d\n",count);  
         return 0;  
     }   
    
  2. 在 menu 目录下使用 make rootfs 生成文件系统, 然后使用qemu、重新挂载内核,文件系统;

     sudo ../bin/qemu-system-x86_64 -kernel linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -s -S -append nokaslr 
    

  1. 新建一个 shell 窗口,用 gdb 调试该 fork 调用;用以下命令在可能运行的函数处添加断点,跟踪fork执行过程;

     b __ia32_sys_fork
     b _do_fork     
     b sys_clone  
     b ret_from_fork 
     b copy_process         
    

  1. 以下为fork的调用过程:
    • 首先在 linux-5.0.1/arch/x86/entry/entry_64.S 中的 ENTRY(interrupt_entry) 处发生中断调用,将 ss , sp , eflags, cs , ip , arigin_ax, return_address 等寄存器的值保存在栈中,从用户态进入到内核态,进行系统调用,之后回到用户态,寄存器出栈,返回值;

    • 进入 linux-5.0.1/arch/x86/entry/entry_compat_64.S 中的 ENTRY(entry_SYSCALL_compat) ,将一些寄存器值入栈,然后调用系统调用处理函数,执行完后,寄存器出栈返回;

    • 进入 linux-5.0.1/arch/x86/entry/common.c 中do_fast_syscall_32 处理函数,执行一些准备工作后,进入do_syscall_32_irqs_on(regs) ,根据系统调用号,查找到 fork 调用,执行对应的调用函数;

    • 在 linux-5.0.1/kernel/fork.c 中执行fork,最终执行 do_fork;

    • do_fork 中调用 copy_process 对子进程进行分配内存,并执行;

    • 在 copy_process 中:

             // 复制父进程进程描述符内容,并返回进程描述符   
             struct task_struct *p;    
             p = dup_task_struct(current, node);   
             // 对子进程栈进行初始化      
             retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);          
      
    • 在 linux-5.0.1/arch/x86/kernel/process_64.c 中 copy_thread_tls :

             // 该句将返回地址置为 ret_from_fork , 因此子进程是从ret_from_fork 开始执行
             frame->ret_addr = (unsigned long) ret_from_fork;
             // 初始化相关寄存器  
             frame->bx = 0;     
             *childregs = *current_pt_regs();         
             childregs->ax = 0;  // 导致 fork 函数返回 0       
      
    • linux-5.0.1/arch/x86/kernel/entry_64.S 中的 ret_from_fork

             ENTRY(ret_from_fork)
                 UNWIND_HINT_EMPTY
                 movq %rax, %rdi
                 call schedule_tail /* rdi: 'prev' task parameter */
                 testq %rbx, %rbx /* from kernel_thread? */
                 jnz 1f /* kernel threads are uncommon */
               2:
                 UNWIND_HINT_REGS
                 movq %rsp, %rdi
                 call syscall_return_slowpath /* returns with IRQs disabled */
                 TRACE_IRQS_ON /* user mode is traced as IRQS on */
                 jmp swapgs_restore_regs_and_return_to_usermode
               1:
                 /* kernel thread */
                 UNWIND_HINT_EMPTY
                 movq	%r12, %rdi
                 CALL_NOSPEC %rbx
                 movq	$0, RAX(%rsp)
                 jmp	2b
             END(ret_from_fork)
      

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

1. 概念

2. exec 调用


四、进程调度

1. 进程调度的时机

2. 跟踪 schedule()

五、总结

  1. 进程是操作系统中的基本运行单位,是操作系统体现其功能的载体,因此为了体现进程功能的繁多,进程描述符包含了非常详实的内容,涵盖了进程生命周期的各个方面;
  2. 操作系统创建新的进程通过执行 fork 调用,分配相应的内存区域,复制父进程的内容,并加入到操作系统进程的执行队列,等待操作系统执行进程调度,从而开始运行新的进程;
  3. 可执行程序可被其它程序通过 execve 系统调用运行,从文件系统中发现相应文件,对其进行解析,根据内容创建新的就绪进程,从而调用该程序;
  4. 操作系统的进程调度是一个复杂的过程,在发生中断、系统调用、或是内核线程主动调用对应函数时发生。这个过程要交换进程的线性区、进程栈内容、相应寄存器内容。

参考

[1] Linux-进程描述符 task_struct 详解: https://www.cnblogs.com/JohnABC/p/9084750.html

[2] 进程的创建 , do_fork()函数详解: https://blog.csdn.net/yunsongice/article/details/5508242

[3] Linux下进程的创建过程分析: https://blog.csdn.net/gatieme/article/details/51569932

[4] Linux进程启动过程分析do_execve: https://blog.csdn.net/gatieme/article/details/51594439