进程描述和创建

进程描述

操作系统通过进程控制块PCB来描述进程,对应Linux内核数据结构struct task_struct
在Linux3.18.6内核中,定义于include/linux/sched.h#1235

pidtgid标识进程
state进程状态
stack进程堆栈
CONFIG_SMP在多处理器时使用
fs文件系统描述
tty控制台
files进程打开文件的文件描述符
mm内存管理描述
signal进程间通信的信号描述
struct list_head tasks管理进程数据结构的双向链表

数据结构list_head,定义于include/linux/types.h#186

struct list_head{
  struct list_head *next, *prev;
};

Linux进程状态
就绪态,运行态,阻塞态

进程状态转换如下:
进程调用do_fork()创建子进程,转为就绪态TASK_RUNNING

  • 就绪态
    当调度器从就绪态中选择一个进程时,转为运行态TASK_RUNNING
  • 运行态
    当高优先级进程抢占或时间片用尽时,转为就绪态TASK_RUNNING
    当进程睡眠等待待定事件或特定资源时,转为阻塞态TASK_RUNNING
    调用do_exit(),进程终止
  • 阻塞态
    当事件发生或资源可用,进程被唤醒,进入就绪队列

操作系统原理中,就绪态和运行态是两个状态,在Linux内核中都是TASK_RUNNING
当进程为TASK_RUNNING状态时,是可运行的,即就绪态,是否运行取决于有没有获得CPU控制权

正在运行的进程,调用用户态库函数exit(),会陷入内核执行do_exit()终止进程,转为TASK_ZOMBIE
TASK_ZOMBIE状态的进程一般称为僵尸进程,内核会在适当的时候处理僵尸进程,释放进程描述符

阻塞态有两种
TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE
前者是可以被信号或wake_up()唤醒,而后者只能被wake_up()唤醒

Linux内核中定义的进程状态,位于include/linux/sched.h#203

进程创建

0号进程初始化

双向链表的第一个节点为init_task
0号进程描述符结构体变量init_task的初始化是通过硬编码方式固定下来
除此之外,其他进程都是通过do_fork()复制父进程的方式进行初始化

init_task变量初始化代码如下,位于init/init_task.c#18

// initial task structure
struct task_struct init_task= INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

其中INIT_TASK宏定义位于include/linux/init_task.h#173

内存管理相关代码

进程描述符结构struct task_struct中管理内存的结构struct mm_struct *mm, *active_mm
mm描述进程地址空间
active_mm描述内存管理

每个进程都有若干个数据段、代码段、堆栈段等,都是由这个数据结构管理

逻辑地址空间分段分页内存管理单元MMU
用来管理物理地址和逻辑地址转换

32位x86体系结构中,进程是4GB的逻辑地址空间

进程之间的父子、兄弟关系

进程描述符struct task_struct中除了双向链表struct list_head tasks管理进程外
记录当前进程的父进程
struct task_struct __rcu* real_parent
struct task_struct __rcu* parent

双向链表记录当前进程的子进程
struct list_head children

双向链表记录当前进程的兄弟进程
struct list_head sibling

保持进程上下文中CPU相关的一些状态信息的数据结构

内核在进程描述符中使用struct thread_struct thread记录进程上下文中CPU状态信息
定义于arch/x86/include/asm/processor.h#468
spip用来保存进程上下文中的ESP寄存器和EIP寄存器状态

进程的创建过程分析

start_kernel()中通过kernel_thread方式(fork方式)创建了两个内核线程:kernel_init和kthreadd内核线程

kernel_init
启动用户态进程init

kthreadd
是所有内核线程的祖先,负责管理所有内核线程

系统启动时,处理0号进程的初始化是通过手动编码创建外
1号init进程的创建实际上是复制0号进程,并修改pid等,然后再加载一个init可执行程序
2号进程kthreadd内核线程也是通过复制0号线程创建

1.用户态创建进程的方法

int pid= fork()
if(pid<0){        // fork error
}else if(pid==0){ // 子进程
}else{}

fork()系统调用复制当前进程,创建了一个子进程,两个进程执行相同的代码

2.fork系统调用
在触发系统调用时,用户态有个int $0x80指令触发中断机制

将当前进程用户态的堆栈SS:ESPCS:EIPEFLAGS压栈到当前进程的内核堆栈,由CPU自动完成
从用户态堆栈转换到内核态堆栈

接下来执行到汇编代码system_call,用于保存现场、执行系统调用内核处理函数、处理完返回、恢复现场

最后iret将CPU关键现场SS:ESPCS:EIPEFLAGS恢复到对应寄存器中
并回到用户态int $0x80的下一条指令

fork()也是一个系统调用
创建一个子进程,复制了父进程所有的进程信息,如内核堆栈、进程描述符等

问题是:当子进程被调度时,是从哪里开始运行的呢?

创建进程相关的几个系统调用内核处理函数,位于kernel/fork.c#1693
可以看出fork()vfork()clone()这三个系统调用和kernel_thread()内核函数都可以创建进程
而且都是通过do_fork()函数创建进程,只不过传递的参数不同

3.fork()内核处理过程
通过fork()创建的父子进程,大部分信息都是一样的,但是如pid值,内核堆栈等信息需要修改

fork一个子进程时,在复制父进程资源过程中,采用了写时复制copy on write技术,不需要修改进程资源,父子进程共享内存存储空间

do_fork()跟踪分析代码,位于kernel/fork.c#1617

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
    子进程创建标志,控制对父进程资源有选择的复制
    标志定义于include/uapi/linux/sched.h#4
  • stack_start
    子进程用户态堆栈的地址
  • regs
    指向pt_regs结构体的指针
    当发生系统调用时,int指令和SAVE_ALL保存现场等操作会将CPU寄存器值按顺序压栈
  • stack_size
    用户态栈大小,通常不需要,总是被设置为0
  • parent_tidptrchild_tidptr
    父进程、子进程用户态下的pid地址

为了便于理解,下述为do_fork()函数体关键代码

struct task_struct* p; // 创建进程描述符指针
int trace= 0;
long nr;               // 子进程pid
...
// 创建子进程的描述符,和执行时所需的其他数据结构
p= copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);
if(!IS_ERR(p)){
  struct completion vfork; // 定义完成量,一个执行单元等待另一个执行单元完成任务
  struct pid* pid;
  ...
  pid= get_task_pid(p, PIDTYPE_PID); // 获取 task 结构体中的 pid
  nr= pid_vnr(pid);                  // 根据 pid 结构体获取进程 pid
  ...
  // 若 clone_flags 包含 CLONE_VFORK 标志,就将完成量 vfork 赋值给进程描述符中的 vfork_done 字段
  // 此处只是对完成量进行初始化
  if(clone_flags & CLONE_FLORK){
    p->vfork_done= &vfork;
    init_completion(&vfork);
    get_task_struct(p);
  }
  wake_up_new_task(p); // 将子进程添加到调度器队列,使之有机会运行

  // forking complete and child started to run, tell ptracer
  ...
  // 若 clone_flags 包含 CLONE_VFORK 标志
  // 就将父进程插入等待队列直到子进程调用 exec()或退出,此处是具体的阻塞
  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; // 返回子进程 pid (父进程的fork()返回值为子进程pid的原因)

do_fork()调用copy_process()完成复制父进程信息、获取pid,调用wake_up_new_task将子进程加入调度器队列,通过clone_flags标志做一些辅助工作
copy_process()说明如下,位于kernel/fork.c#1174

static struct task_struct* copy_process(){
  int retval;
  struct task_struct* p;
  ...
  retval= security_task_create(clone_flags); // 安全性检查
  ...
  p= dup_task_struct(current); // 复制PCB,为子进程创建内核栈,进程描述符
  ftrace_graph_init_task(p);
  ...

  retval= -EAGAIN;
  // 检查该用户的进程数是否超过限制
  if(atomic_read(&p->real_cred->user->processes)>=task_rlimit(p,RLIMIT_NPROC)){
    // 检查用户是否具有相关权限,不一定是root
    if(p->real_cred->user!=INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN)){
      goto bad_fork_free;
    }
    ...
    // 检查进程数量是否超过 max_threads,后者取决于内存大小
    if(nr_threads>=max_threads){
      goto bad_fork_cleanup_count;
    }
    if(!try_module_get(task_thread_info(p)->exec_domain->module)){
      goto bad_fork_cleanup_count;
    }
    ...
    spin_lock_init(&p->alloc_lock); // 初始化自旋锁
    init_sigpending(&p->pending);   // 初始化挂起进程
    posix_cpu_timers_init(p);       // 初始化cpu定时器
    ...
    // 初始化新进程调度程序数据结构,将新进程的状态设置为TASK_RUNNING,并禁止内核抢占
    retval= sched_fork(clone_flags, p);
    ...
    // 复制所有进程信息
    shm_init_task(p);
    retval= copy_semundo(clone_flags, p);
    ...
    retval= copy_files(clone_flags, p);
    ...
    retval= copy_fs(clone_flags, p);
    ...
    retval= copy_sighand(clone_flags, p);
    ...
    retval= copy_signal(clone_flags, p);
    ...
    retval= copy_mm(clone_flags, p);
    ...
    retval= copy_namespaces(clone_flags, p);
    ...
    retval= copy_io(clone_flags, p);
    ...
    retval= copy_thread(clone_flags, stack_start, stack_size, p); // 初始化子进程内核栈
    ...
    // 若传入的 pid 指针和全局结构体变量 init_struct_pid 的地址不同
    // 则为子进程分配新的 pid
    if(pid!=&init_struct_pid){
      retval= -ENOMEM;
      pid= alloc_pid(p->nsproxy->pid_ns_for_children);
      if(!pid){
        goto bad_fork_cleanup_io;
      }
    }
    ...
    p->pid= pid_nr(pid); // 根据 pid 结构体获取进程 pid
    // 若 clone_flags 包含 CLONE_THREAD 标志,说明子进程和父进程在同一个线程组
    if(clone_flags & CLONE_THREAD){
      p->exit_signal= -1;
      p->group_leader= current->group_leader; // 将线程组的 leader 设为子进程的组 leader
      p->tgid= current->tgid;                 // 子进程继承父进程的 tgid
    }else{
      if(clone_flags & CLONE_PARENT){
        p->exit_signal= current->group_leader->exit_signal;
      }else{
        p->exit_signal= (clone_flags & CSIGNAL);
      }
      p->group_leader= p; // 子进程的组 leader 是其自己

      p->tgid= p->pid; // 组号 tgid 是其自己的 pid
    }
    ...
    if(){
      ptrace_init_task();
      init_task_pid();
      if(){
      }else{
      }
      attach_pid(p, PIDTYPE_PID);
      nr_threads++; // 增加系统的进程数
    }
    ...
    return p; // 返回被创建的子进程描述符指针
    ...
  }
}

copy_process()调用dup_task_struct()复制当前进程描述符task_struct、信息检查、初始化、设置进程状态为TASK_RUNNING(此时子进程为就绪态)、采用写时复制技术复制进程资源,调用copy_thread()初始化子进程内核栈、设置子进程pid等

dup_task_struct()说明如下,位于kernel/fork.c#305

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);     // 为子进程创建进程描述符分配存储空间
  ...
  ti= alloc_thread_info_node(tsk, node); // 实际上创建了两个页,一部分用来存放 thread_info,另一部分是内核栈
  ...
  err= arch_dup_task_struct(tsk, orig);  // 复制父进程的 task_struct 信息
  ...
  tsk->stack= ti; // 将栈底的值赋给新节点的 stack
  // 对子进程的 thread_info 结构进行初始化,复制父进程的 thread_info 结构,然后将 task 指针指向子进程的进程描述符
  setup_thread_stack(tsk, orig);
  ...
  return tsk; // 返回新创建的进程描述符指针
  ...
}

thread_info结构,被称为小型的进程描述符,内存区域大小为8KB,占据连续两个页框
通过task指针指向进程描述符,该结构体位于arch/x86/include/asm/thread_info.h#26

内核栈由高地址向低地址增长,C语言中thread_info由低地址向高地址增长
内核通过屏蔽ESP寄存器的低13有效位获取thread_info结构的基地址

在较新的内核代码中,task_struct没有直接指向thread_info结构的指针,而是用一个void指针表示,然后通过类型转换来访问该结构

内核栈和thread_info结构被定义在一个联合体中,alloc_thread_info_node分配一个联合体
thread_union定义于include/linux/sched.h#2241

union thread_union{
  struct thread_info thread_info;
  unsigned long stack[THREAD_SIZE/sizeof(long)];
};

4.内核堆栈关键信息的初始化
dup_task_struct()为子进程分配了内核栈,copy_thread()才能真正完成内核栈关键信息的初始化
copy_thread()代码说明如下,位于arch/x86/kernel/process_32.c#132

int copy_thread(){
  struct pt_regs* childregs= task_pt_regs(p);
  struct task_struct* tsk;
  int err;

  p->thread.sp= (unsigned long)childregs;
  p->thread.sp0= (unsigned long)(childregs+1);
  memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

  if(unlikely(p->flags & PF_KTHREAD)){
    // kernel thread
    memset(childregs, 0, );
    // 若创建
    p->thread.ip= (unsigned long)ret_from_kernel_thread;
    task_user_gs(p)= __KERNEL_STACK_CANARY;
    childregs->ds= __USER_DS;
    childregs->es= __USER_DS;
    childregs->fs= __KERNEL_PERCPU;
    childregs->bx= sp; // function
    childregs->bp= arg;
    childregs->orig_ax= -1;
    childregs->cs= __KERNEL_CS | get_kernel_rpl();
    childregs->flags= X86_EFLAGS_IF | X86_EFLAGS_FIXED;
    p->thread.io_bitmap_ptr= NULL;
    return 0;
  }
  // 复制内核堆栈
  *childregs= * current_pt_regs();
  childregs->ax= 0; // 将子进程的 eax 置0,所以 fork 的子进程返回值为 0
  ...
  // ip 指向 ret_from_fork,子进程从此处开始执行
  p->thread.ip= (unsigned long)ret_from_fork;
  task_user_gs(p)= get_user_gs(current_pt_regs());
  ...
  return err;
}

对子进程开始执行的起点ret_from_kernel_thread内核线程或ret_from_fork用户态进程
以及在子进程中fork()系统调用的返回值,都进行了注释

posted @ 2024-12-05 13:07  sgqmax  阅读(26)  评论(0)    收藏  举报