Ming's Blog

Back

当我们fork时发生了什么Blur image

fork代码

可以看到 fork 实际上是调用了 kernel_clone 函数,我们对其展开。

kernel_clone

  1. 首先用 copy_process 进行对父进程的上下文复制,内存空间拷贝(如果是fork调用则拷贝且为写时复制)等操作。
  2. trace_sched_process_fork 跟踪新任务的创建事件,并建议在唤醒新线程前做这件事,防止进程退出太快导致访问无效指针。
  3. wake_up_new_task 将任务放入请求队列并激活。

copy_process

copy_process完整代码很长,这里仅截取关键操作。

函数声明

"linux/kernel/fork.c"

static __latent_entropy struct task_struct *copy_process(
					struct pid *pid,
					int trace,
					int node,
					struct kernel_clone_args *args)
c

flag判断

对各类标志位进行判断,检测新建立的进程(线程)是否符合要求。

延迟信号

linux强制在此点之前接收到的任何信号在 fork 发生之前就被处理,同时收集并延迟 fork 期间收到的信号,使他们看起来像是在 fork 之后发生的。

  1. 首先用 sigemptyset 初始化一个信号集,随后用 INIT_HLIST_NODE 初始化一个哈希链表节点。
  2. if (!(clone_flags & CLONE_THREAD)) 代表如果不是在进行线程创建(即为进程创建),则将延迟的信号节点加入当前进程的信号链表中。
  3. dup_task_struct 复制当前进程(线程)的任务结构。
  4. if (args->io_thread) 判断如果为处理IO的thread,则标记为 IO_WORKER,同时初始化阻塞信号集,并屏蔽除 SIGKILLSIGSTOP 之外的所有信号。

初始化

  1. 根据标志位设置set_child_tidclear_child_tid为提供的tid或者null
  2. copy_creds复制当前进程的凭据(credentials)到新创建的进程或线程的任务结构体(task_struct)中,其中包括thread的用户id和组id等。
  3. if (is_ucounts_overlimit(task_ucounts(p), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC)))检测进程数是否超过限制,如果超过限制,并且创建任务的用户既不是初始用户也不具备系统资源(CAP_SYS_RESOURCE)或系统管理员(CAP_SYS_ADMIN)能力,则跳转至bad_fork_cleanup_count进行清理。
  1. if (data_race(nr_threads >= max_threads))首先检查thread数量是否超限。
  2. rcu_copy_process
  3. 初始化task_struct成员,包括信号挂起列表,各类计数器,各类标志位等等
  4. sched_fork初始化调度器。
  5. 设置内核子系统:perf_event_init_task设置性能事件,audit_alloc设置审计框架。

复制操作

  1. shm_init_task初始化对System V共享内存的访问
  2. security_task_alloccopy_semundocopy_filescopy_fscopy_sighandcopy_signalcopy_mmcopy_namespacescopy_io分别进行对安全、信号量撤销、打开的文件描述符、文件系统、信号处理器、信号状态、内存管理、命名空间、IO上下文相关信息的复制。
  3. copy_thread设置新线程的栈、栈大小以及线程局部存储(TLS)。
  4. stackleak_task_init初始化新thread的内核栈,stackleak是一种安全特性。
  5. alloc_pid先检查pid != &init_struct_pid即新进程(线程)不为初始进程, 之后分配pid给thread。

trace_sched_process_fork

跟踪新任务的创建事件,并建议在唤醒新线程前做这件事,防止进程退出太快导致访问无效指针。

wake_up_new_task

  1. 定义局部变量:
    • struct rq_flags rf; 用于存储请求队列(runqueue)的标志状态。
    • struct rq *rq; 是指向请求队列的指针,请求队列用于存储和管理待运行任务。
  2. WRITE_ONCE 写入新建任务状态为 TASK_RUNNING
  3. 请求队列初始化:
    • rq = __task_rq_lock(p, &rf); 上锁
    • update_rq_clock(rq) 更新时钟信息
    • post_init_entity_util_avg(p); 初始化任务的平均利用率统计信息,为调度器的负载平衡(load balancing)和任务选择提供数据。
  4. 激活任务:
    • activate_task(rq, p, ENQUEUE_NOCLOCK); 将新任务放入请求队列并准备调度。
    • trace_sched_wakeup_new(p); 如果启用了跟踪(tracepoint),则记录唤醒新任务的事件。
    • check_preempt_curr(rq, p, WF_FORK); 检查当前运行的任务是否应当被新任务抢占(preempt)。
    • task_rq_unlock(rq, p, &rf); 解锁

put_pid

释放pid结构体所占资源。

  1. 计算 struct pid_namespace * 对应的引用计数,每当调用 alloc_pid 则使计数+1,调用 put_pid 则计数-1,为0时则释放资源。
  2. EXPORT_SYMBOL_GPL(put_pid); 这个宏的作用是导出 put_pid 函数的符号,使得其他模块可以在GPL兼容的许可证条款下使用它。

其它

当我们fork时发生了什么
https://astro-pure.js.org/blog/linux-fork
Author Ming
Published at February 20, 2024
Comment seems to stuck. Try to refresh?✨