Linux内核创建一个新进程的过程
“平安的祝福 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
进程在创建它的时刻开始存活。在Linux系统中,这通常是fork()系统的结果,该系统调用通过复制一个现有的进程来创建一个全新的进程。只有在创建init进程时,是通过代码实现数据结构的填充。调用fork()的进程称为父进程,新生的进程称为子进程。在系统调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核两次返回:一次回到父进程,另一次回到创建的新的子进程。
进程的描述符--task_struct (下面是中英文大体注释)
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 进程的状态 void *stack; //进程通过alloc_thread_info函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。 unsigned int flags; /* per process flags, defined below */ 进程的标签 //进程的调度 int on_rq; int prio, static_prio, normal_prio;//进程优先级 实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)。值越大静态优先级越低。 unsigned int rt_priority;//rt_priority用于保存实时优先级。normal_prio值取决于静态优先级和调度策略,static_prio用于保存静态优先级,可以通过nice系统调用来进行修改。 const struct sched_class *sched_class;//sched_class结构体表示调度类 struct sched_entity se; struct sched_rt_entity rt; struct sched_dl_entity dl; unsigned int policy; //policy表示进程的调度策略 int nr_cpus_allowed; cpumask_t cpus_allowed;//cpus_allowed用于控制进程可以在哪里处理器上运行。 struct list_head tasks; //用于构建进程链表 ,内核的双向循环链表的实现方法 - 一个更简略的双向循环链表 struct mm_struct *mm, *active_mm;//mm指向进程所拥有的内存描述符,而active_mm指向进程运行时所使用的内存描述符。对于普通进程而言,这两个指针变量的值相同。但是,内核线程不 拥有任何内存描述符,所以它们的mm成员总是为NULL。当内核线程得以运行时,它的active_mm成员被初始化为前一个运行进程的 active_mm值。 /* per-thread vma caching */ u32 vmacache_seqnum; struct vm_area_struct *vmacache[VMACACHE_SIZE]; /* task state */ int exit_state; int exit_code, exit_signal;//exit_code用于设置进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。 int pdeath_signal; /* The signal sent when the parent dies *///exit_signal被置为-1时表示是某个线程组中的一员。只有当线程组的最后一个成员终止时,才会产生一个信号,以通知线程组的领头进程的父进程。 unsigned int jobctl; /* JOBCTL_*, siglock protected */ /* Used for emulating ABI behavior of previous Linux versions */ unsigned int personality; unsigned in_execve:1; /* Tell the LSMs that the process is doing an * execve */ unsigned in_iowait:1; /* Revert to default priority/policy when forking */ unsigned sched_reset_on_fork:1; unsigned sched_contributes_to_load:1; unsigned long atomic_flags; /* Flags needing atomic access. */ pid_t pid; //进程标识号 pid_t tgid; /*程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系 * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->real_parent->pid) */ struct task_struct __rcu *real_parent; /* real parent process */ struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ /* * children/sibling forms the list of my natural children */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent's children list */ struct task_struct *group_leader; /* threadgroup leader */ cputime_t utime, stime, utimescaled, stimescaled;//utime/stime用于记录进程在用户态/内核态下所经过的节拍数(定时器)。utimescaled/stimescaled也是用于记录进程在用户态/内核态的运行时间,但它们以处理器的频率为刻度。 cputime_t gtime;//gtime是以节拍计数的虚拟机运行时间(guest time)。 unsigned long nvcsw, nivcsw; /* context switch counts */ u64 start_time; /* monotonic time in nsec */ u64 real_start_time; /* boot based time in nsec */ /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ unsigned long min_flt, maj_flt; struct task_cputime cputime_expires; struct list_head cpu_timers[3]; /* process credentials */ const struct cred __rcu *real_cred; /* objective and real subjective task * credentials (COW) */ const struct cred __rcu *cred; /* effective (overridable) subjective task * credentials (COW) */ char comm[TASK_COMM_LEN]; /* executable name excluding path - access with [gs]et_task_comm (which lock it with task_lock()) - initialized normally by setup_new_exec */ /* file system info */ int link_count, total_link_count; /* CPU-specific state of this task */ struct thread_struct thread; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; /* namespaces */ struct nsproxy *nsproxy; /* signal handlers */ struct signal_struct *signal;//signal指向进程的信号描述符。 struct sighand_struct *sighand;//sighand指向进程的信号处理程序描述符。 sigset_t blocked, real_blocked;//blocked表示被阻塞信号的掩码,real_blocked表示临时掩码。 sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */ struct sigpending pending; //pending存放私有挂起信号的数据结构。 unsigned long sas_ss_sp;// sas_ss_sp是信号处理程序备用堆栈的地址,sas_ss_size表示堆栈的大小。 size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data;//设备驱动程序常用notifier指向的函数来阻塞进程的某些信号(notifier_mask是这些信号的位掩码),notifier_data指的是notifier所指向的函数可能使用的数据。 sigset_t *notifier_mask; struct callback_head *task_works; struct audit_context *audit_context;//进程审计 /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id; /* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed, * mempolicy */ spinlock_t alloc_lock; /* Protection of the PI data structures: */ raw_spinlock_t pi_lock; /* journalling filesystem info */ void *journal_info; /* stacked block device info */ struct bio_list *bio_list; /* VM state */ struct reclaim_state *reclaim_state; struct backing_dev_info *backing_dev_info; struct io_context *io_context; unsigned long ptrace_message; siginfo_t *last_siginfo; /* For ptrace use. */ struct task_io_accounting ioac; struct rcu_head rcu; /* * cache last used pipe for splice */ struct pipe_inode_info *splice_pipe; struct page_frag task_frag; /* * time slack values; these are used to round up poll() and * select() etc timeout values. These are in nanoseconds. */ unsigned long timer_slack_ns; unsigned long default_timer_slack_ns;
分配进程描述符
Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色。
struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ unsigned long flags; /* low level flags */ __u32 status; /* thread synchronous flags */ __u32 cpu; int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit; /* thread address space */ struct restart_block restart_block; unsigned long previous_sp; /* sp of previous stack in case of nested IRQ stacks */ __u8 supervisor_stack[0]; };
进程的状态:
//include/linux/Sched.h
#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8
进程的创建
Linux通过clone()系统调用实现fork()。然后又有clone()去调用do_fork()。实际上从kernel/fork.c文件中,我们可以看到 __ARCH_WANT_SYS_FORK,__ARCH_WANT_SYS_VFORK,__ARCH_WANT_SYS_CLONE都是调用do_fork(),只不过传递的参数不同。
do_fork()完成了创建中大部分工作,它定义在kernel/fork.c文件中。调用copy_process()函数,通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符(子进程的描述符)。
p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);//struct task_struct *p;
copy_process()
以下是对copy_process()函数分成几段,进行简单注释。
1.
/*为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时父子进程的的描述符完全相同。*/ p = dup_task_struct(current);//struct task_struct *p;
2.
/*检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制*/
retval = -EAGAIN;
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
current->flags &= ~PF_NPROC_EXCEEDED;
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
retval = -EAGAIN;
if (nr_threads >= max_threads)//检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。
goto bad_fork_cleanup_count;
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
3.
/*子进程开始与父进程区别开来。进程描述符内的许多成员都要被清0或者设为初始值。那些不是继承来的信息主要是统计信息。task_struct中的大多数数据都依然未被修改*/
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);//更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV被置为0
p->flags |= PF_FORKNOEXEC;//表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
rcu_copy_process(p);
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock);
init_sigpending(&p->pending);
p->utime = p->stime = p->gtime = 0;
p->utimescaled = p->stimescaled = 0;
p->default_timer_slack_ns = current->timer_slack_ns;
task_io_accounting_init(&p->ioac);
acct_clear_integrals(p);
posix_cpu_timers_init(p);
p->start_time = ktime_get_ns();
p->real_start_time = ktime_get_boot_ns();
p->io_context = NULL;
p->audit_context = NULL;
if (clone_flags & CLONE_THREAD)
threadgroup_change_begin(current);
cgroup_fork(p);
4.
/* Perform scheduler related setup. Assign this task to a CPU. */
/*调度器设置。调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。
并且,为了不对其他进程的调度产生影响,此时子进程共享父进程的时间片。*/
retval = sched_fork(clone_flags, p);
retval = perf_event_init_task(p);
retval = audit_alloc(p);
5.
/* copy all the process information */
shm_init_task(p);
/*复 制进程的所有信息。根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构。
在一般的情况下,这些资源会被给定的进程所有线程共享;否则,这些资源对每个进程时不同的。
比如copy_semundo()、复制开放文件描 述符(copy_files)、复制符号信息(copy_sighand 和 copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。*/
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_mm(clone_flags, p);
retval = copy_io(clone_flags, p);
retval = copy_thread(clone_flags, stack_start, stack_size, p);
6.
/*分配pid。用alloc_pid函数为这个新进程分配一个pid,Linux系统内的pid是循环使用的,采用位图方式来管理。
简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。成功则赋给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;
}
7.
1 /*最后,是一些扫尾工作并返回一个指向子进程的指针*/
total_forks++;//进程总数加1
2 spin_unlock(¤t->sighand->siglock);
3 syscall_tracepoint_update(p);
4 write_unlock_irq(&tasklist_lock);
5
6 proc_fork_connector(p);
7 cgroup_post_fork(p);
8 if (clone_flags & CLONE_THREAD)
9 threadgroup_change_end(current);
10 perf_event_fork(p);
11
12 trace_task_newtask(p, clone_flags);
13 uprobe_copy_process(p, clone_flags);
14
15 return p;
dump_task_struct()
dump_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时父子进程的的描述符完全相同。
//在dump_task_struct()函数中
tsk = alloc_task_struct_node(node);//struct task_struct *tsk; ti = alloc_thread_info_node(tsk, node);//struct thread_info *ti; err = arch_dup_task_struct(tsk, orig);//int err;
tsk->stack = ti;
setup_thread_stack(tsk, orig);
#define alloc_task_struct_node(node) \ ({ \ struct page *page = alloc_pages_node(node, GFP_KERNEL | __GFP_COMP, \ KERNEL_STACK_SIZE_ORDER); \ struct task_struct *ret = page ? page_address(page) : NULL; \ \ ret; \ })
alloc_task_struct_node(node)函数中创建页节点,其中一部分就用于存放thread_info节点,另一部分存放堆栈。
alloc_thread_info_node(tsk, node);//实际用于分配内核堆栈空间
arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)函数复制一个PCB——task_struct
在arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)函数
*dst = *src;//在arch_dup_task_struct()函数中,通过赋值完成复制操作
setup_thread_stack()函数只是复制thread_info,而非复制内核堆栈
static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org) { *task_thread_info(p) = *task_thread_info(org); task_thread_info(p)->task = p; }
copy_thread()函数
在copy_thread()函数中,完成初始化
//在copy_thread(unsigned long clone_flags, unsigned long sp,unsigned long arg, struct task_struct *p)函数中
struct pt_regs *childregs = task_pt_regs(p);//pt_regs内核堆栈的栈底,p代表子进程 struct task_struct *tsk; int err; p->thread.sp = (unsigned long) childregs; p->thread.sp0 = (unsigned long) (childregs+1);
*childregs = *current_pt_regs();//复制内核堆栈数据
childregs->ax = 0;//为什么子进程的fork返回0,这里就是原因!
if (sp)
childregs->sp = sp;//调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
struct pt_regs {
unsigned long bx;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long bp;
unsigned long ax;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
};
从 pt_regs的结构可以看出pt_regs里面存放的是一些要压入栈的内容和一些寄存器和返回值。所以pt_regs是表示内核栈底
子进程从什么时候开始执行??
就是从p->thread.ip = (unsigned long) ret_from_fork;开始的,第一条指令是ret_from_fork,那么我们查找entry_32.S得到
ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl_cfi %eax
pushl_cfi $0x0202 # Reset kernel eflags
popfl_cfi
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)
其中调用syscall_exit,
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
ASM_CLAC
pushl_cfi %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
syscall_after_call:
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
restore_all:
TRACE_IRQS_IRET
syscall_exit在system_call中已经简单分析过了,就是检测调度和出栈,用于返回用户的空间。这样就完成了子进程的用户空间的切换,可以执行子进程的程序。
实验结果
1.设置断点
2.menu中输入fork命令
3.在copy_thread单步调试
4.在ret_from_work中调试
参考文献: