应用程序调用glibc的fork, fork发起系统调用SyS_clone(sys_clone),SyS_clone中主要调用了do_fork。
关注了下内核中do_fork的流程,遗留的问题有:
(1)关于进程的相关进程号的知识;
(2)调度域;
(3)调度实体中的一些统计量的意义;
(4)父子进程的信号处理关系;
(5)copy_mm()---->dup_mm()---->dup_mmap(),
dup_mmap()没看懂;
(6)其它,很多;
关于新建进程时的vruntime的处理需要注意:新建时是减去了min_vruntime,而后在入队时又加上了min_vruntime。
以下分析为系统调用时由库自身传递的参数,与创建内核线程不同,中间涉及的一些队列及统计量的处理也只考虑了新建进程的处理情形,没有考虑进程被唤醒时的情形。
do_fork
do_fork()----task_struct *p;
| p = copy_process(clone_flags, stack_start,
| stack_size, child_tidptr, NULL, trace); | clone_flags:CLONE_CHILD_SETTID|
| CLONE_CHILD_CLEARTID|
| SIGCHLD, | stack_start: NULL, s
| tack_size: NULL,
| child_tidptr: &THREAD_SELF->tid, | trace: 0 | |---->wake_up_new_task(p)
copy_process
copy_process()----task_struct *p = dup_task_struct(current); |----rt_mutex_init_task(p);
| 初始化实时互斥量表头,用于处理优先级反转问题
| |----p->did_exec = 0; 没有执行execve() |----copy_flags(clone_flags, p); | 即: unsigned long new_flags = p->flags; | new_flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
| 权限设置
| | new_flags |= PF_FORKNOEXEC;还没有被调度运行 | p->flags = new_flags; |----init_sigpending(&p->pending); 没有等待的信号需要处理
| |----do_posix_clock_monotonic_gettime(&p->start_time);
| 初始化start_time
| |----sched_fork(p); |----copy_files(clone_flags, p) 拷贝文件信息(打开的文件) |----copy_fs(clone_flags, p) 拷贝文件系统信息(当前目录,根目录) |----copy_sighand(clone_flags, p) 拷贝信号处理函数 |----copy_signal(clone_flags, p)??????没看明白,存疑 |----copy_mm(clone_flags, p) |----copy_thread(clone_flags, stack_start, stack_size, p) | |----struct pid *pid = alloc_pid(p->nsproxy->pid_ns) |----p->pid = pid_nr(pid); |----p->tgid = p->pid; |----if(clone_flags & CLONE_THREAD) | p->tgid = current->tgid; | |----p->set_child_tid = child_tidptr; |----p->clear_child_tid = child_tidptr; | |----p->pdeath_signal = 0; |----p->exit_state = 0; |----p->group = p; |----p->task_works = NULL; |----p->read_parent = current; |----p->parent_exec_id = current->self_exec_id;
wake_up_new_task
wake_up_new_task()---->set_task_cpu(p,
| select_task_rq(p, SD_BALANCE_FORK,0);
| 为进程p选择运行队列 |----p->se.cfs_rq = tg->cfs_rq[cpu]; |----p->se.parent = tg->se[cpu]; |----p->rt.rt_rq = tg->rt_rq[cpu]; |----p->rt.parent = tg->rt_se[cpu]; |----active_task(rq, p, 0);将p插入到选择的运行队列 |---->enqueue_task(rq, p, 0); |----p->sched_class->enqueue_task(rq, q, 0); | 即enqueue_task_fair(rq, q, 0); |----p->on_rq = 1; |----rq->curr->sched_class->
| check_preempt_curr(rq, q, 0);
| 设置当前进程被抢占 |----rq->skip_clock_update = 1;
wake_up_new_task中的相关调用
enqueue_task_fair(rq, q, 0)---->enqueue_entity(cfs_rq, se, 0); enqueue_entity(cfs_rq, se, 0); |---->se->vruntime += cfs_rq->min_vruntime; |---->__enqueue_entity(cfs_rq, se); 新建进程挂入运行队列 |---->se->on_rq = 1;
copy_process中的相关调用
dup_task_struct
dup_task_struct()----struct task_struct *tsk; | arch_dup_task_struct(tsk, orig);完全复制task_struct |----struct thread_info *ti; | tsk->stack = ti; 设置进程task_struct的内核态栈 |----setup_thread_stack(tsk, orig);
| 完全复制thread_info, | 并将thread_info的task指向新建的线程,
| 建立了thread_info与task_struct关系
| |----clear_tsk_need_resched(tsk);
| 将新建线程的threa_info中flags的TIF_NEED_RESCHED置0
sched_fork
sched_fork()---->__sched_fork() |----p->on_rq = 0; |----p->se.on_rq = 0; |----p->se.exec_start = 0; |----p->se.sum_exec_runtime = 0; |----p->se.prev_sum_exec_runtime = 0; |----p->se.nr_migrations = 0; |----p->se.vruntime = 0; |----get_cpu() |----preempt_disable() |----p->state = TASK_RUNNING; |----p->prio = current->normal_prio; |----if (!rt_prio(p->prio)) | p->sched_class = &fair_sched_class; |----p->sched_class->task_fork() 即task_fork_fair(p) | 在系统调用过程中,中断是打开的,因此中断可抢占当前运行的进程,
| 系统时钟可得到更新(schedule_tick()), | 注意此处没有参考服务器上的unicore内核代码 | |----task_thread_info(p)->preempt_count = 1 |----put_cpu() |----preempt_enable() | 将会检查当前运行进程是否可被抢占, | 如果可以,则触发内核态抢占。 | (不是子进程,因为当前运行的进程还没有进行 | wake_up_new_task(p))唤醒新进程
task_fork_fair
task_fork_fair()----struct sched_entity *curr, *se; |----curr = cfs_rq->curr当前队列上运行的进程 |----se = &p->se 新建进程的调度实体 sched_entity |----if(curr) |---- se->vruntime = curr->vruntime
| 先把新建进程的vruntime设成父进程的vruntime,与理论模型不符
| |----place_entity(cfs_rq, se, 1) | 即vruntime = cfs_rq->min_vruntime; | vruntime += sched_vslice(cfs_rq, se); | se->vruntime =
| max_vruntime(se->vruntime, vruntime);
| 此处修正新建进程的vruntime;
| |----如果系统设置了子进程先运行,则: | swap(curr->vruntime, se->vruntime) | resched_task(rq->curr)
| 将当前进程的thread_info中flag置位TIF_NEED_RESCHED
| |----se->vruntime -= cfs_rq->min_vruntime | ???? | 务必结合enqueue_task函数一起看 | dequeue_task中也会将se->vruntime减去cfs_rq->min_vruntime
copy_files
copy_files()----struct files_struct *oldf, *newf | oldf = current->files |----newf = dup_fd(oldf, &error) |----tsk->files = newf
dup_fd
dup_fd()----关于dup_fd(),先做下简要说明: | task_struct中有files_struct实例的指针files指向该实例 | | 结构体files_struct中的struct file *fd_array[NR_OPEN_DEFAULT]
| 存放每一个文件的信息; | 因为我们想知道那些文件的关闭或带开,因此 | 结构体files_struct中又引入了:
| unsigned long close_on_exec_init[1]; | unsigned long open_fds_init[1]; | 那么关于上面的三个数据有有怎样整合到一起呢?引入fdtable结构体; | fdtable中分别存放的就是file数组的起始地址,
| close_on_exec_init地址,open_fds_init地址; | | 同样fdtable实例地址也进行了存放,赋给files_struct实例中的
| struct fdtable *fdt; | | 因此,files_struct中存放: | (1)fd_array[NR_OPEN_DEFAULT]、close_on_exec_init[1]、
| open_fds_init[1]; | (2)fdtab,用于存放上面三个实体的地址; | (3)fdt,用于存放fdtab的地址; | | dup_fd()的业务在于生成新的files_struct实例,在该实例中保持
| fdt与fdtab的关系, | fdtab与fd_array、close_on_exec_init、open_fds_init的关系; | | 问题在于我们对于新生成的数组fd_array[NR_OPEN_DEFAULT]与父进程中的
| fd_array[NR_OPEN_DEFAULT]的关系如何处理?
| 代码给出的答案是:
| 子进程的fd_array[NR_OPEN_DEFAULT]复制父进程中的fd_array的相应元数, | 并且将相应文件的引用计数“加1”。 | |struct files_struct *newf....kmem_cache_alloc(); |----newf->counst = 1;
copy_fs
copy_fs()----tsk->fs = copy_fs_struct(fs) |---->新分配struct fs_struct *fs |----fs->umask = old->umask; |----fs->root = old->root; |----path_get(&fs->root); |----fs->pwd = old->pwd; |----path_get(&fs->pwd);
copy_sighand
copy_sighand()----新分配struct sighand_struct *sig; |----rcu_assign_pointer(tsk->sighand, sig); |----memcpy(sig->action, current->sighand->action,
| sizeof(sig->action)); | struct sigaction
| {__sighandler_t sa_handler;
| unsigned long sa_flags;
| sigset_t sa_mask}
copy_mm
copy_mm()----tsk->nvcsw = tsk->nivcsw = 0; |----tsk->mm = NULL; |----tsk->active_mm = NULL; |----oldmm = current->mm;考虑普通进程中创建子进程,因此oldmm不会是NULL |----mm = dup_mm(tsk); |----tsk->mm = mm; |----tsk->active_mm = mm;
dup_mm
dup_mm()----struct mm_struct *mm, *oldmm = current->mm; |----mm = allocate_mm(); |----memcpy(mm, oldmm, sizeof(struct mm_struct))
| 注意一级页表的基址与oldmm相同;
| |----mm_init(mm, tsk)
| 新建了自己的页表,用户空间部分页表项清零,
| 内核与I/O空间拷贝了init_pgd的相关页表项;
| |----init_new_context(tsk, mm); |----dump_mm_exe_file(oldmm, mm); |----mm->exe_file = old->exe_file; |----dup_mmap(mm, oldmm) ?????没看懂,以后再看,涉及内存管理
mm_init
mm_init()----mm->mm_users = 1; mm->mm_count = 1; |----mm->free_area_cache = TASK_UNMAPPED_BASE; mmap映射区起始地址 |----mm_init_owner(mm, p); |----mm_alloc_pgd(mm); |---->mm->pgd = pgd_alloc(mm);
page_alloc
因为我所接触的SOC为unicore,unicore的页表中提供了ACCESSED、DIRTY、EXIST、Cache able、RWX等类似与x86的标志,l龙芯中>也有专门的Dirty标志位,但是我在ARM中并未看到(好像有Write buffer, cache able);
那么我想问在在ARM上是实现了什么样的页调度策略时,以及如何实现。
pgd_alloc()----pgd_t *new_pgd = __pgd_alloc() 分配一级页表,并取得首地址
|----memset(new_pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
| 将用户空间部分清0
|----memcpy(new_pgd + USER_PTRS_PER_PGD,
| init_pgd + USER_PTRS_PER_PGD,
| (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
| 从init_pgd中拷贝内核和I/O空间的一级页表项(原来和师兄讨论过,得证)
copy_thread
copy_thread()---->memset(&thread->cpu_context, 0,
| sizeof(struct cpu_context_save) | 将子进程的上下文的寄存器清0
|---->struct pt_regs *childregs = task_pt_regs(p); | *childregs = *current_pt_regs(void),
| 复制父进程中的保存的寄存器与状态寄存器到子进程.
| | childregs->ARM_r0 = 0; 子进程返回父进程的值为0. |---->thread->cpu_context.pc = (unsigned int) ret_from_fork;
| 设置子进程被调度运行时的开始地址.
| |---->thread->cpu_context.sp = (unsigned int) childregs;
| 设置子进程内核态栈的"sp"值.