我们都知道进程是linux内核中最为重要的一个抽象概念,那么我们平时在fork一个进程时,该进程究竟是咋么产生的呢?
本篇博文会浅谈一下在进程创建过程中扮演着重要角色的do_fork函数
1.内核如何来抽象一个进程
内核通过一个叫做task_struct的结构体来抽象一个进程
该结构体的定义(以内核2.6为例)在include/linux.sched.h中
截取部分task_struct如下
task_struct{
volatile long state;
void *stack;
atomic_t usage;
unsigned int flags;
unsigned int ptrace;
int lock_depth;
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 char fpu_counter;
struct list_head tasks;
struct plist_node pushable_tasks;
struct mm_struct *mm, *active_mm;
pid_t pid;
struct task_struct *real_parent;
struct task_struct *parent;
struct list_head children;
struct fs_struct *fs;
struct files_struct *files;
struct signal_struct *signal;
}
上述task_struct属性是我节选出的部分其结构体中的属性,我们从中可以大致了解到标识一个进程的属性大致会有该用以表示该进程所处的状态,进程的标志,以及进程是否被其他进程跟踪,进程锁的深度,进程的优先级,进程的pid,进程的父母,进程的孩子链表,进程所打开的文件描述符表,进程所处的文件系统,进程的信号。。。。等等一堆我们平时可能遇到的和进程相关的东西
2.do_fork简单分析
接触linuxC编程的人都知道,创建一个进程我们需要调用fork函数,fork其实又是调用了clone函数来实现的,而clone函数中最关键的函数就是do_fork函数。
在分析do_fork前我们脑海中可以大致想象一下,进程究竟是如何被创建出来的,假如让你来创建一个进程你会咋么做?
我们可以这样去分析,既然原来的进程被抽象成一个task_struct,那么新进程也是一个task_struct只不过它里面的一些属性会不同与原来的task_struct,那么创建一个新进程所要做的工作就是赋值一个与原来进程一样都的task_struct结构,然后然后将新进程的task_struct不同于原来task_struct的属性进行修改即可
do_fork定义在kernel/fork.c文件中
1.在分析该函数之前我们先来分析一下它的函数的各个参数
参数如下
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
个参数具体含义如下
1.clone_flags:该参数是此函数中最重要的一个参数,该值中的每个位都代表对子进程task_struct中的每种属性的设置
2.stack_start:子进程用户态堆栈的开始地址
3.regs:当系统发生系统调用时,需从用户态切换到内核态,此结构体用来保存此时用户态进程中的通用寄存器中的值,并被存放在内核态堆栈中
4.stack_size:目前未被使用,通常设为0
5.parent_tidptr:父进程在用户态下pid的地址
6.child_tidptr:子进程在用户态下pid的地址
其中clone_flags的标志位宏定义如下:
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD 0x00010000 /* Same thread group? */
#define CLONE_NEWNS 0x00020000 /* New namespace group? */
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED 0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
#define CLONE_STOPPED 0x02000000 /* Start in stopped state */
#define CLONE_NEWUTS 0x04000000 /* New utsname group? */
#define CLONE_NEWIPC 0x08000000 /* New ipcs */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
#define CLONE_IO 0x80000000 /* Clone io context */
举个简单的例子当我们的参数中设置了CLONE_VM这个宏,那么就以为这我们新创建的进程和其父进程要共享VM,当我们设置了CLONE_FILES时意味这父子进程之间共享打开的文件描述符
do_fork开始执行后首先做的就是为子进程定义一个新的task_struct指针
struct task_struct *p;
在下来会检查一些clone_flags所不允许的位组合
例如:
if (clone_flags & CLONE_NEWUSER) {
if (clone_flags & CLONE_THREAD)
return -EINVAL;
上述中不允许同时既设置了CLONE_NEWUSER标志,还设置CLONE_THREAD标志,这样就会产生错误
类似上面当一系列的安全检查完毕之后,copy_process函数就登场了
copy_process函数工作流程具体如下:
1)调用dup_task_struct函数为新的进程创建一个内核栈,thread_info结构和task_struct等,当然此时的值都是和父进程完全一样的
dup_task_struct函数定义如下
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
unsigned long *stackend;
int err;
prepare_to_copy(orig);
//为tsk分配内存空间
tsk = alloc_task_struct();
if (!tsk)
return NULL;
//为ti分配内存空间
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
赋值orig属性给新的tsk
err = arch_dup_task_struct(tsk, orig);
if (err)
goto out;
tsk->stack = ti;
//初始化进程缓存脏数据
err = prop_local_init_single(&tsk->dirties);
if (err)
goto out;
//设置线程栈空间
setup_thread_stack(tsk, orig);
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; /* for overflow detection */
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int();
#endif
/* One for us, one for whoever does the "release_task()" (usually parent) */
atomic_set(&tsk->usage,2);
atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
account_kernel_stack(ti, 1);
return tsk;
out:
free_thread_info(ti);
free_task_struct(tsk);
return NULL;
}
2)检查并确保新创建该子进程后,当前用户所拥有的进程数没有超出给它分配的资源限制
代码如下
if (atomic_read(&p->real_cred->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->real_cred->user != INIT_USER)
goto bad_fork_free;
}
3)子进程着手使自己与父进程区别开来,从父进程那继承过来的许多属性都要被清0或设置一个初始值,但task_struct中的大多数数据还是未被修改
部分代码如下
spin_lock_init(&p->alloc_lock);
init_sigpending(&p->pending);
p->utime = cputime_zero;
p->stime = cputime_zero;
p->gtime = cputime_zero;
p->utimescaled = cputime_zero;
p->stimescaled = cputime_zero;
p->prev_utime = cputime_zero;
p->prev_stime = cputime_zero;
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->lock_depth = -1; /* -1 = no lock */
do_posix_clock_monotonic_gettime(&p->start_time);
p->real_start_time = p->start_time;
monotonic_to_bootbased(&p->real_start_time);
p->io_context = NULL;
p->audit_context = NULL;
#ifdef CONFIG_TRACE_IRQFLAGS
p->irq_events = 0;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
p->hardirqs_enabled = 1;
#else
p->hardirqs_enabled = 0;
#endif
p->hardirq_enable_ip = 0;
p->hardirq_enable_event = 0;
p->hardirq_disable_ip = _THIS_IP_;
p->hardirq_disable_event = 0;
p->softirqs_enabled = 1;
p->softirq_enable_ip = _THIS_IP_;
p->softirq_enable_event = 0;
p->softirq_disable_ip = 0;
p->softirq_disable_event = 0;
p->hardirq_context = 0;
p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
p->lockdep_depth = 0; /* no locks held yet */
p->curr_chain_key = 0;
p->lockdep_recursion = 0;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif
p->bts = NULL;
4)给子进程分配一个CPU
代码如下:
sched_fork(p, clone_flags);
5)
接着就是子进程拷贝父进程的一些资源,具体如下
调用copy_files函数拷贝父进程打开的文件描述符
748 static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
{
struct files_struct *oldf, *newf;
int error = 0;
oldf = current->files;
if (!oldf)
goto out;
if (clone_flags & CLONE_FILES) {
//增加文件描述符的引用计数
atomic_inc(&oldf->count);
goto out;
}
//复制文件描述符
newf = dup_fd(oldf, &error);
if (!newf)
goto out;
tsk->files = newf;
error = 0;
out:
return error;
}
调用copy_fs继承父进程所属的文件系统
static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{
struct fs_struct *fs = current->fs;
if (clone_flags & CLONE_FS) {
/* tsk->fs is already what we want */
write_lock(&fs->lock);
if (fs->in_exec) {
write_unlock(&fs->lock);
return -EAGAIN;
}
//将该文件系统的用户数加1
fs->users++;
write_unlock(&fs->lock);
return 0;
}
//将加1后的fs拷贝给进程的fs
tsk->fs = copy_fs_struct(fs);
if (!tsk->fs)
return -ENOMEM;
return 0;
}
调用copy_signal函数拷贝并设置新的signal_struct
signal_struct包含了大量的进程运行的信息
调用copy_mm函数处理与新进程的内存问题
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
//如果是vfork则共享内存,此处为增加引用计数
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
//新分配一块内存,并将父进程内存中的数据copy到新内存中,说明2.6内核并没有fork的写时拷贝
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
/* Initializing for Swap token stuff */
mm->token_priority = 0;
mm->last_interval = 0;
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
调用copy_io函数拷贝父进程的I/O情况
static int copy_io(unsigned long clone_flags, struct task_struct *tsk)
{
#ifdef CONFIG_BLOCK
struct io_context *ioc = current->io_context;
if (!ioc)
return 0;
/*
* Share io context with parent, if CLONE_IO is set
*/
if (clone_flags & CLONE_IO) {
tsk->io_context = ioc_task_link(ioc);
if (unlikely(!tsk->io_context))
return -ENOMEM;
} else if (ioprio_valid(ioc->ioprio)) {
tsk->io_context = alloc_io_context(GFP_KERNEL, -1);
if (unlikely(!tsk->io_context))
return -ENOMEM;
tsk->io_context->ioprio = ioc->ioprio;
}
#endif
return 0;
}
还有调用copy_namespaces 和 copy_thread等,这里就不在赘述
6)调用alloc_pid为新进程分配一个pid
pid = alloc_pid(p->nsproxy->pid_ns);
7)copy_process做一些收尾工作,并返回新进程的task_struct指针
此时再次回到了do_fork,新创建的子进程被唤醒,并让其先投入运行
if (unlikely(clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
__set_task_state(p, TASK_STOPPED);
} else {
//换新新的进程
wake_up_new_task(p, clone_flags);
}
到这里本篇关于do_fork的博文也就基本结束了
3.总结
关于进程创建的源码理解,我感觉主要抓住俩点即可。第一进程被内核抽象成了啥?它的数据结构是咋样的(task_struct)这点我们必须有所认识,第二创建进程最主要的其实就是拷贝父进程的task_struct里的属性,但是关键点是拷贝哪些,哪些又是子进程和父进程所不同的,很简单我们只需要把握住进程创建函数里的clone_flags参数就可以知道咋么拷贝了。