在<Linux内核之execve函数>中,我们知道execve是一个新程序运行的开始,那么很多要问fork呢?fork又有什么区别呢?fork通常是在编程中,为了创建多个进程而使用的API。他和execve的差别是:execve属于进程替换,通过<Linux内核之execve函数>分析,仔细的朋友应该发现了execve函数在启动新程序的时候,还是用的之前老程序的task_struct结构。因此,这样它的pid就不会变,只是需要从内存重新分配他需要的页表和内存。而fork则不一样,它会产生一个完全独立的新进程,新的task_struct结构,新的pid的,只是会有一个写时复制的功能(刚刚fork出来的进程和原进程一模一样,内存什么都一样,只有当前进程内存发生写访问的时候,会重建立新的映射表)。这里先不说映射表重映射的问题(COW机制),这个问题之后讲映射的时候会讲。
下面我们看看fork函数在内核的实现:
点击(此处)折叠或打开
-
-
-
long do_fork(unsigned long clone_flags,
-
unsigned long stack_start,
-
unsigned long stack_size,
-
int __user *parent_tidptr,
-
int __user *child_tidptr)
-
{
-
// stack_start是新程序占地址,stack_size栈大小
-
return _do_fork(clone_flags, stack_start, stack_size,
-
parent_tidptr, child_tidptr, 0);
-
}
-
-
SYSCALL_DEFINE0(vfork)
-
{ // 可以看到vfork产生的新进程内存空间和原始进程一样CLONE_VM
-
return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL, 0);
-
}
-
-
SYSCALL_DEFINE0(fork)
-
{
-
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
-
}
-
-
// 内核线程创建,也是调用的do_fork,只是参数不同
-
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
-
{
-
// 注意,内核线程在stack_start和stack_sz传入的是线程回调函数和线程传参
-
return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
-
(unsigned long)arg, NULL, NULL, 0);
-
}
其中SYSCALL_DEFINE0是一个宏,表示这是一个不包含任何参数的系统调用,这个宏会产生一个sys_fork/sys_vfork函数。也就是当应用程序调用fork函数的时候,CPU产生系统调用中断,中断处理函数通过查表(sys_call_table),通过对应的系统调用号找到sys_fork函数并开始执行(R7/W8存放的系统调用号,参数以此类推),sys_fork函数到_do_fork并没有做什么操作,只是参数不同(fork:SIGCHLD, vfork:CLONE_VFORK),这里kernel_thread、线程功能也会混着一起讲,因此现在直接到_do_fork函数,调用栈如下:
点击(此处)折叠或打开
-
fork (user space)
-
---|----------------------------------------------------------------------------
-
v (kernel space)
-
el0_sync (arm64同步异常中断处理函数)
-
el0_svc (查找sys_call_table,获取到sys_execve函数地址,并运行)
-
sys_fork
-
do_fork
-
_do_fork
_do_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)
-
{
-
struct task_struct *p;
-
int trace = 0;
-
long nr;
-
-
// ptrace相关设置,主要判断进入do_fork是哪个系统调用引起的
-
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,p是新进程的task_struct
-
p = copy_process(clone_flags, stack_start, stack_size,
-
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
-
add_latent_entropy();
-
// 如果进程子进程复制成功,主进程就进入if语句,子进程不会进入这里,子进程直接进入
-
// ret_from_fork
-
if (!IS_ERR(p)) {
-
struct completion vfork;
-
struct pid *pid;
-
-
trace_sched_process_fork(current, p);
-
// 父进程获取新进程p的pid
-
pid = get_task_pid(p, PIDTYPE_PID);
-
nr = pid_vnr(pid);
-
-
if (clone_flags & CLONE_PARENT_SETTID)
-
put_user(nr, parent_tidptr);
-
// 查看父进程是否调用的vfork,如果是就初始化vfork_done完成函数
-
if (clone_flags & CLONE_VFORK) {
-
p->vfork_done = &vfork;
-
init_completion(&vfork);
-
get_task_struct(p);
-
}
-
// 将新进程p加入到run queue即加入到ready调度队列,此后p就会开始后参与调度
-
wake_up_new_task(p);
-
-
/* forking complete and child started to run, tell ptracer */
-
if (unlikely(trace))
-
ptrace_event_pid(trace, pid);
-
// 如果是vfork产生的进程,需要等待子进程先运行,直到子进程complete了父进程,
-
// vfork函数才返回。因此,vfork产生的子进程,总是优先于父进程运行。
-
-
// vfork保证子进程先运行,在它调用exec或exit之后父进程才能调度运行,也就是当子
-
// 进程调用exec或者exit的时候, 会触发wake_up($vfork)的信号。
-
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);
-
}
-
// 父进程在这里返回,返回子进程的pid号,这个pid会修改父进程返回的pt_regs[0]的值
-
// 子进程的返回值在copy_thread_tls里面已经设置,子进程不会运行这里
-
-
return nr;
-
}
可以知道,fork是把当前进程复制一个一模一样的进程,除了栈、LR,其他内容都一样。
在do_fork函数中实现的wake_up_new_task函数在copy_process函数之后调用,主要是用于将子进程加入调度队列。它的实现如下:
点击(此处)折叠或打开
-
void wake_up_new_task(struct task_struct *p)
-
{
-
struct rq_flags rf;
-
struct rq *rq;
-
-
raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
-
// 设置新进程P的状态为TASK_RUNNING
-
p->state = TASK_RUNNING;
-
#ifdef CONFIG_SMP
-
// 进程添加到CPU负载均衡
-
__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
-
#endif
-
// 获取当前p进程所在CPU的运行队列rq
-
rq = __task_rq_lock(p, &rf);
-
post_init_entity_util_avg(&p->se);
-
// 将进程p加入到rq队列,具体在以后进程调度章节说明(RUNNING状态)
-
activate_task(rq, p, 0);
-
p->on_rq = TASK_ON_RQ_QUEUED;
-
trace_sched_wakeup_new(p);
-
check_preempt_curr(rq, p, WF_FORK);
-
-
#ifdef CONFIG_SMP
-
// SMP相关,这里占时不知道干什么的
-
if (p->sched_class->task_woken) {
-
p->sched_class->task_woken(rq, p);
-
}
-
#endif
-
task_rq_unlock(rq, p, &rf);
-
}
在do_fork函数中的copy_process函数是今天的重点函数,该函数实现了进程复制的所有操作,实现如下:
点击(此处)折叠或打开
-
static __latent_entropy struct task_struct *copy_process(
-
unsigned long clone_flags,
-
unsigned long stack_start,
-
unsigned long stack_size,
-
int __user *child_tidptr,
-
struct pid *pid,
-
int trace,
-
unsigned long tls,
-
int node)
-
{
-
int retval;
-
struct task_struct *p;
-
-
// 这里省略掉一些参数检测
-
-
// 将当前进程复制一份给新进程p
-
retval = -ENOMEM;
-
p = dup_task_struct(current, node);
-
if (!p)
-
goto fork_out;
-
-
ftrace_graph_init_task(p);
-
-
rt_mutex_init_task(p);
-
-
// 省略一些无用,chinaunix总说是敏感词汇的代码。
-
-
// 复制信用凭证
-
retval = copy_creds(p, clone_flags);
-
if (retval < 0)
-
goto bad_fork_free;
-
// 超过最大进程,返回
-
retval = -EAGAIN;
-
if (nr_threads >= max_threads)
-
goto bad_fork_cleanup_count;
-
-
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
-
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
-
p->flags |= 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);
-
// 初始化信号挂起pending
-
init_sigpending(&p->pending);
-
-
// 这里省略初始化和时间等相关的杂项
-
-
// 初始化调度实体se,和调度器类,调度器章节将会描述
-
// 如果存在p->sched_class->task_fork,就执行
-
retval = sched_fork(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_policy;
-
// perf相关,这里不描述
-
retval = perf_event_init_task(p);
-
if (retval)
-
goto bad_fork_cleanup_policy;
-
//审计相关,这里不描述
-
retval = audit_alloc(p);
-
if (retval)
-
goto bad_fork_cleanup_perf;
-
// 进程共享内存链表初始化
-
shm_init_task(p);
-
retval = copy_semundo(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_audit;
-
// 将当前current进程的打开的fd描述复制一份到新进程->p->files
-
retval = copy_files(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_semundo;
-
// 将current进程的工作目录复制一份到新进程(pwd, /)-》p->fs
-
retval = copy_fs(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_files;
-
// 将Current进程的信号处理函数,赋值一份到新进程->p->sighand
-
retval = copy_sighand(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_fs;
-
// 通过current初始化一份信号共享结构(线程信号共享就用的这个结构,还有一些其他数据)
-
// p->signal
-
retval = copy_signal(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_sighand;
-
// 将current的映射页表拷贝一份到新进程,并设置成只读,为什么这么设置,
-
// 以后的虚拟内存章节会讲解。
-
// 即这就是为什么刚fork后,两个进程内存里面的值是一样的
-
retval = copy_mm(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_signal;
-
// 命名空间继承current的,即同一个命名空间
-
retval = copy_namespaces(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_mm;
-
// 赋值IO空间,tsk->io_context或者new_ioc->ioprio
-
retval = copy_io(clone_flags, p);
-
if (retval)
-
goto bad_fork_cleanup_namespaces;
-
// 拷贝每个进程不同的部分,这里非常重要,稍后详细说明
-
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
-
if (retval)
-
goto bad_fork_cleanup_io;
-
-
// 如果线程不是init_struct_pid(idle进程pid),就从新申请一个Pid
-
if (pid != &init_struct_pid) {
-
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
-
if (IS_ERR(pid)) {
-
retval = PTR_ERR(pid);
-
goto bad_fork_cleanup_thread;
-
}
-
}
-
//省略一些赋值
-
-
// 如果是线程,则设置线程组组长为当前进程指向的组长,如果是进程则将组长设置成自己,
-
// 并初始化退出码
-
p->pid = pid_nr(pid);
-
if (clone_flags & CLONE_THREAD) { // 线程运行这里
-
p->exit_signal = -1;
-
p->group_leader = current->group_leader;
-
p->tgid = current->tgid; // 进程的tgid才是真的pid 即同一个线程组下的线程tgid相同,pid不同
-
} 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;
-
p->tgid = p->pid; //没有线程时,tgid==pid
-
}
-
-
p->nr_dirtied = 0;
-
p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
-
p->dirty_paused_when = 0;
-
-
p->pdeath_signal = 0;
-
INIT_LIST_HEAD(&p->thread_group);
-
p->task_works = NULL;
-
-
// cgroup相关,这里不说明
-
threadgroup_change_begin(current);
-
retval = cgroup_can_fork(p);
-
if (retval)
-
goto bad_fork_free_pid;
-
-
write_lock_irq(&tasklist_lock);
-
-
// 设置父进程,如果是线程,父进程就是current的父进程。否则current就是新进程的父进程
-
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
-
p->real_parent = current->real_parent;
-
p->parent_exec_id = current->parent_exec_id;
-
} else {
-
p->real_parent = current;
-
p->parent_exec_id = current->self_exec_id;
-
}
-
-
// 省略一些与此无关的
-
-
//
-
if (likely(p->pid)) {
-
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
-
-
//初始化pid结构到
-
init_task_pid(p, PIDTYPE_PID, pid);
-
if (thread_group_leader(p)) {//新进程是线程组组长,同样意味着它是进程 而不是线程
-
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));//将pid赋值到task->pid[PGID].pid
-
init_task_pid(p, PIDTYPE_SID, task_session(current));//将pid赋值到task->pid[SID].pid
-
-
// 是否是进程收养者,是,就设置成当前Pid空间的收养者(do_exit函数里面会用到)
-
if (is_child_reaper(pid)) {
-
ns_of_pid(pid)->child_reaper = p;
-
p->signal->flags |= SIGNAL_UNKILLABLE;
-
}
-
-
p->signal->leader_pid = pid;//因为是组长,因此将它的pid记录到leader_pid
-
p->signal->tty = tty_kref_get(current->signal->tty);
-
// 将进程p添加到它父进程的children链表
-
list_add_tail(&p->sibling, &p->real_parent->children);
-
// 将进程p添加到Init_task下面,即idle进程的tasks链表下面
-
list_add_tail_rcu(&p->tasks, &init_task.tasks);
-
// 将p添加到对应的pgid和sid链表(组,会话)
-
attach_pid(p, PIDTYPE_PGID);
-
attach_pid(p, PIDTYPE_SID);
-
__this_cpu_inc(process_counts);
-
} else {// 不是组长,说明是线程,增加线程数量
-
current->signal->nr_threads++;
-
atomic_inc(¤t->signal->live);//增加线程live数量
-
atomic_inc(¤t->signal->sigcnt);
-
list_add_tail_rcu(&p->thread_group, // 将线程p挂到组长的thread_group链表下面
-
&p->group_leader->thread_group);
-
list_add_tail_rcu(&p->thread_node, // 将线程p挂到共享信号的thread_head链表下面(信号使用)
-
&p->signal->thread_head);
-
}
-
// 添加到pid空间,并增加线程数
-
attach_pid(p, PIDTYPE_PID);
-
nr_threads++;
-
}
-
-
total_forks++;
-
//省略一些不重要的
-
-
// 返回新进程task_struct
-
return p;
-
// 省略失败处理细节
-
}
在copy_process函数中的dup_task_struct函数实现如下,该函数用于从当前进程current的task_struct结构中,复制一份一模一样的给新进程p:
点击(此处)折叠或打开
-
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
-
{
-
struct task_struct *tsk;
-
unsigned long *stack;
-
struct vm_struct *stack_vm_area;
-
int err;
-
-
// 这里传入的参数是NUMA_NO_NODE,表示申请内存的时候,从当前进程所在内存结点申请;内存结点将在伙伴子系统加以说明,这里不讲解;
-
if (node == NUMA_NO_NODE)
-
node = tsk_fork_get_node(orig);
-
-
// 从内存node结点申请一段内存作为新进程的tsk结构
-
tsk = alloc_task_struct_node(node);
-
if (!tsk)
-
return NULL;
-
-
// 从node结点申请一个新的栈地址
-
stack = alloc_thread_stack_node(tsk, node);
-
if (!stack)
-
goto free_tsk;
-
-
// 得到一个stack的vm_struct结构,这里不讲解
-
stack_vm_area = task_stack_vm_area(tsk);
-
-
// 这个函数 其实就是tsk = orig,即将current进程memcpy到tsk。
-
err = arch_dup_task_struct(tsk, orig);
-
-
// 将stack地址设置到tsk的记录栈地址的位置,替换掉从current拷贝来的栈(这是内核栈,
-
// 每个应用程序有两个栈,一个应用栈,一个内核栈;而内核线程只有内核栈.
-
// 因此,这里可以统一申请内核栈)
-
tsk->stack = stack;
-
// 如果定义了VMAP_STACK,就将stack vm_struct结构赋值
-
#ifdef CONFIG_VMAP_STACK
-
tsk->stack_vm_area = stack_vm_area;
-
#endif
-
// 增加引用计数
-
#ifdef CONFIG_THREAD_INFO_IN_TASK
-
atomic_set(&tsk->stack_refcount, 1);
-
#endif
-
-
if (err)
-
goto free_stack;
-
-
#ifdef CONFIG_SECCOMP
-
tsk->seccomp.filter = NULL;
-
#endif
-
// 将current的threadinfo结构拷贝一份到tsk,threadinfo待会儿会详细介绍。
-
setup_thread_stack(tsk, orig);
-
clear_user_return_notifier(tsk);
-
clear_tsk_need_resched(tsk);
-
// 设置栈的结束位置,用于检测栈溢出用的
-
set_task_stack_end_magic(tsk);
-
-
// 省略一些无关紧要的赋值
-
-
account_kernel_stack(tsk, 1);
-
-
kcov_task_init(tsk);
-
// 返回新进程的task_struct结构
-
return tsk;
-
}
-
-
static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
-
{
-
// 赋值current的thread_info结构,然后将task指向新进程
-
*task_thread_info(p) = *task_thread_info(org);
-
task_thread_info(p)->task = p;
-
}
-
在申请新的进程task_struct结束后,我这里有必要说说进程的thread_info结构。thread_info结构是存放到内核的SP栈中的,原本Linux是直接将task_struct存放到SP当中,但是后来随着task_struct越来越大,因此,添加了一个thread_info结构做中间代理结构,达到通过sp能够获取到对应进程的task_struct结构的目的。thread_info在栈的位置如图:
如上图, 通常内核的栈大小为2个page,即8K,并且内核栈严格按照8K对齐,因此当获取到SP的指针的时候(sp current),只需要与上8k取反的值(sp & (~8k))就可以得到thread_info结构。thread_info->task就指向该进程的task_struct结构。由于内核栈是向下增长,因此,如果栈越界,首先破坏的是自己的thread_info结构,然后崩溃(咋们内核中使用的current起始就是一个宏,利用当前SP和thread_info反向找到自己的task_struct结构的,有兴趣的可以看看这个宏)。
copy_process函数中实现的copy_files函数比较简单,如下:
点击(此处)折叠或打开
-
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;
-
}
-
// 复制一份current的files成newf
-
newf = dup_fd(oldf, &error);
-
if (!newf)
-
goto out;
-
// 将复制出来的newf赋值给files
-
tsk->files = newf;
-
error = 0;
-
out:
-
return error;
-
}
-
因此,从copy_files这一个简单的说明后,其他的copy_*函数,这里就不多做说明了, 有兴趣的,可以自己去看看,先主要说说copy_thread_tls函数。
copy_thread_tls是一个非常重要的函数,其主要是设置新进程的运行地址和寄存器值,实现如下:
点击(此处)折叠或打开
-
struct cpu_context {
-
unsigned long x19;
-
unsigned long x20;
-
unsigned long x21;
-
unsigned long x22;
-
unsigned long x23;
-
unsigned long x24;
-
unsigned long x25;
-
unsigned long x26;
-
unsigned long x27;
-
unsigned long x28;
-
unsigned long fp;
-
unsigned long sp;
-
unsigned long pc;
};
-
asmlinkage void ret_from_fork(void) asm("ret_from_fork");
-
-
// 这个函数是和体系结构强相关的,这里我看的是armv8处理器
-
int copy_thread(unsigned long clone_flags, unsigned long stack_start,
-
unsigned long stk_sz, struct task_struct *p)
-
{
-
// 获取pt_regs地址: task->stack + THREAD_START_SP -1
-
// 其中task->stack起始存放的是thread_info,THREAD_START_SP = 16k - 16,
-
// 即在栈的最高地址存放的是寄存器值, pt_regs。
-
struct pt_regs *childregs = task_pt_regs(p);
-
-
// 将新进程的内核态需要的寄存器信息清0
-
memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));
-
-
// 发现新进程是一个应用进程,进入if语句
-
if (likely(!(p->flags & PF_KTHREAD))) {
-
// 复制current进程的用户态寄存器到新进程
-
*childregs = *current_pt_regs();
-
// 将新进程的返回值设置成0,regs[0] 就是X0寄存器,在ARM中X0作为返回值寄存器使用
-
// 因此当fork函数返回后,在子进程中,返回值是0
-
childregs->regs[0] = 0;
-
-
-
*task_user_tls(p) = read_sysreg(tpidr_el0);
-
// 如果用户设置了栈的起始地址,就是设置用户栈的地址是stack_start
-
// 用户进程有两个栈,用户态的栈和内核态的栈,内核态的栈前面dup_task_struct的
-
// 时候申请了,这里是设置用户态的栈,由用户态指定
-
if (stack_start) {
-
if (is_compat_thread(task_thread_info(p)))
-
childregs->compat_sp = stack_start;
-
else
-
childregs->sp = stack_start;
-
}
-
-
// TLS相关的
-
if (clone_flags & CLONE_SETTLS)
-
p->thread.tp_value = childregs->regs[3];
-
} else { // 内核线程进入else语句
-
// 内核线程将childregs的CPU寄存器请0
-
memset(childregs, 0, sizeof(struct pt_regs));
-
childregs->pstate = PSR_MODE_EL1h;//设置CPU工作模式是EL1模式(用户进程在EL0模式)
-
if (IS_ENABLED(CONFIG_ARM64_UAO) &&
-
cpus_have_cap(ARM64_HAS_UAO))
-
childregs->pstate |= PSR_UAO_BIT;
-
// X19存放stack_start,从kernel_thread传入参数可以知道stack_start是线程回调函数
-
// X20存放内核线程传入参数(stk_sz:内核线程传入的线程函数传参)
-
p->thread.cpu_context.x19 = stack_start;
-
p->thread.cpu_context.x20 = stk_sz;
-
}
-
// 不管是内核线程还是应用线程,新进程执行的第一个函数时钟是ret_from_fork
-
p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
-
// 设置CPU的栈指向childregs结构,方便返回的时候,弹出到寄存器。
-
p->thread.cpu_context.sp = (unsigned long)childregs;
-
-
ptrace_hw_copy_thread(p);
-
-
return 0;
-
}
-
-
static inline int copy_thread_tls(
-
unsigned long clone_flags, unsigned long sp, unsigned long arg,
-
struct task_struct *p, unsigned long tls)
-
{
-
return copy_thread(clone_flags, sp, arg, p);
-
}
经过了copy_thread_tls函数之后,新进程的运行环境就已经准备就绪,最后只需要将新进程的task_struct结构加入到run queue调度队列里面即可(wake_up_new_task函数)。一旦调度到新的进程,新的进程就从ret_from_fork函数开始运行,其中ret_from_fork函数实现如下:
点击(此处)折叠或打开
-
ret_to_user:
-
disable_irq // 禁止中断
-
ldr x1, [tsk, #TI_FLAGS] // 读取tsk结构的TI_FLAGS位到x1寄存器
-
and x2, x1, #_TIF_WORK_MASK // 判断tsk的TI_FLAGS位是否置位
-
cbnz x2, work_pending // 这里用于检查是否有工作挂起。
-
finish_ret_to_user:
-
enable_step_tsk x1, x2
-
kernel_exit 0 // 退出内核态EL1到EL0
-
ENDPROC(ret_to_user)
-
-
ENTRY(ret_from_fork)
-
bl schedule_tail // 进程调度的时候 会说明
-
cbz x19, 1f //从copy_thread_tls函数知道x19如果不为0那么,此进程就为内核线程。X19存放的是内核线程的回调函数。
-
mov x0, x20 //从copy_thread_tls函数知道,如果是内核线程,那么x20内核线程的回调函数的传入参数
-
blr x19 // x0存放传入的第一个参数,然后跳转到内核线程回调函数去执行。
-
1: get_thread_info tsk // 用户线程运行这里,通过ret_to_user返回到用户空间
-
b ret_to_user // 返回到应用空间
-
ENDPROC(ret_from_fork)
应用程序fork的调用栈如下:
注:copy_mm函数会在以后讲解虚拟地址的时候详细讲解。
阅读(3673) | 评论(0) | 转发(0) |