文章目录
- 1. 引言
- 2. 进程&线程——概念
- 3. 进程控制块/进程描述符(PCB)
- 4. 进程内核栈(Kernel Stack)
- 5. 进程 ID(PID)
- 6. 进程状态
- 7. 进程创建
- 7.1. 进程之间的关系
- 7.2. 进程创建的写时拷贝机制(copy-on-write)
- 7.3. fork() 函数的入口
- 7.4. 创建新进程的核心函数 kernel_clone()
- 7.5. kernel_clone() 的核心函数 copy_process()
- 7.6. 实现 fork() 写时拷贝的关键所在 copy_process() 中的 copy_mm()
- 7.7. copy_process() 的核心函数 dup_task_struct()
- 7.8. 分配进程描述符 alloc_task_struct_node()
- 7.9. 分配进程内核栈 alloc_thread_stack_node()
- 7.10. fork() 函数的简要总体框图
- 8. exit() 进程退出
- 8.1. exit() 系统调用的定义
- 8.2. do_exit() 函数
- 8.2.0. do_exit() 的参数和返回值
- 8.2.1. 检查和同步线程组退出
- 8.2.2. 清理与调试相关的资源
- 8.2.3. 取消 I/O 和信号处理
- 8.2.4. 检查线程组是否已终止
- 8.2.5. 释放系统资源
- 8.2.6. 释放线程和调度相关资源
- 8.2.7. 通知父进程
- 8.2.8. 减少进程栈引用计数,设置进程状态 DEAD 并切换进程
- 8.2.9. 进程消亡
- 8.2.10. put_task_stack() 释放内核栈
- 8.2.11. release_task_stack()
- 8.2.12. free_thread_stack()
- 8.2.13. thread_stack_delayed_free()
- 8.2.14. thread_stack_free_rcu()
- 8.2.15. try_release_thread_stack_to_cache()
- 8.3. do_exit() 函数小结
- 9. 阻塞等待退出
- 10. 进程退出为什么延时销毁?
- 11. 遗留问题
- 12. 处理遗留问题
- 13. 线程
- 14. 创建线程
- 15. 第三章总结
- #附
- #01. 计算 task_struct 大小
- #02. 验证内核栈大小
- #03. 验证内核栈结构
- #04. 验证栈指针位置
- #05. init 进程
1. 引言
进程是 Linux
操作系统抽象概念中最基本的一种。本文将和读者一起学习有关 Linux
内核中对进程、线程的概念;然后再一起研究在 Linux
内核中是如何管理每个进程,本文只涉及对进程以及线程生命周期的介绍(进程是如何创建、消亡)。我们的操作系统存在的目的就是为了更好的运行用户程序,由此可见进程管理在操作系统中的地位可谓是至关重要的。而有关进程调度内容笔者将放在下一篇文章中详述,虽然这些都属于进程管理的范畴,不过由于笔者是按照《Linux 内核设计与实现》第三版并结合 Linux 6.15.0-rc2
内核代码来展开讨论,因此这里的探讨顺序将会与书籍原文保持一致。
2. 进程&线程——概念
在计算机科学领域,有关进程(Process
)较为官方的定义是,进程是“正在执行的程序实例”。与静态存储在磁盘上的程序文件不同,进程具有动态的执行上下文,包括程序计数器(PC
)、寄存器集、堆栈以及进程控制块(PCB
)中记录的资源分配信息。与之对应衍生出的概念线程(Thread
),较为官方的定义是,线程是“正在执行的程序指令序列中的最小可独立调度单元”。它包含程序计数器、寄存器状态、堆栈以及线程控制块(TCB
)等上下文信息。线程也被称为轻量级进程,因为它与同属一个进程的其他线程共享同一地址空间和打开的文件描述符等资源。
[注]:这里笔者从 Wiki 偷过来一张图供读者们稍微形象的理解——此处为照顾之前没有学习过操作系统的同学们。
事实上,进程就是正在执行的程序代码的实时结果。内核的调度对象是线程,而非进程。在 Linux
内核中事实上并不特别区分进程和线程。对于 Linux
而言线程只不过是一种特殊的进程而已。进程的另一个名字也叫 task
,Linux
内核也通常把进程叫做任务(task
),文中提到的任务、进程所指的都是进程。
3. 进程控制块/进程描述符(PCB)
操作系统中用于记录和管理每个进程所有信息的结构称为进程控制块(Process Control Block
,简称 PCB
)。在 Linux
系统中这种结构也会被称为进程描述符(Process Descriptor
)。Linux
内核中将进程列表存放在一个任务队列(task list
)的双向循环链表中。链表中的每一项类型为 task_struct
结构体类型,这个类型也就是 Linux
内核中的进程描述符,在该结构体中保存了一个操作系统中进程的所有通用信息。
 task_struct
结构体相对比较大,在笔者当前的 64
位机器下,该结构体大约占用 13.43KB
。#01(数据的计算过程见文末附 #01
)由于现代计算机对性能要求越来越高,因此现代操作系统体积越来越庞大,用于管理进程的数据结构保存的信息也越来越多,因此进程描述符的体积也逐渐增加。
task_struct
结构体中的成员非常多,现在就暂且看看就行,等真正遇见再回过头来仔细分析其含义和作用。
// linux 6.15.0-rc2
// PATH: include/linux/sched.h
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
#endif
unsigned int __state;
/* saved state for "spinlock sleepers" */
unsigned int saved_state;
/*
* This begins the randomizable portion of task_struct. Only
* scheduling-critical items should be added above here.
*/
randomized_struct_fields_start
void *stack;
refcount_t usage;
/* Per task flags (PF_*), defined further below: */
unsigned int flags;
unsigned int ptrace;
#ifdef CONFIG_MEM_ALLOC_PROFILING
struct alloc_tag *alloc_tag;
#endif
#ifdef CONFIG_SMP
int on_cpu;
struct __call_single_node wake_entry;
unsigned int wakee_flips;
unsigned long wakee_flip_decay_ts;
struct task_struct *last_wakee;
/*
* recent_used_cpu is initially set as the last CPU used by a task
* that wakes affine another task. Waker/wakee relationships can
* push tasks around a CPU where each wakeup moves to the next one.
* Tracking a recently used CPU allows a quick search for a recently
* used CPU that may be idle.
*/
int recent_used_cpu;
int wake_cpu;
#endif
int on_rq;
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
struct sched_dl_entity *dl_server;
#ifdef CONFIG_SCHED_CLASS_EXT
struct sched_ext_entity scx;
#endif
const struct sched_class *sched_class;
#ifdef CONFIG_SCHED_CORE
struct rb_node core_node;
unsigned long core_cookie;
unsigned int core_occupation;
#endif
#ifdef CONFIG_CGROUP_SCHED
struct task_group *sched_task_group;
#endif
#ifdef CONFIG_UCLAMP_TASK
/*
* Clamp values requested for a scheduling entity.
* Must be updated with task_rq_lock() held.
*/
struct uclamp_se uclamp_req[UCLAMP_CNT];
/*
* Effective clamp values used for a scheduling entity.
* Must be updated with task_rq_lock() held.
*/
struct uclamp_se uclamp[UCLAMP_CNT];
#endif
struct sched_statistics stats;
#ifdef CONFIG_PREEMPT_NOTIFIERS
/* List of struct preempt_notifier: */
struct hlist_head preempt_notifiers;
#endif
#ifdef CONFIG_BLK_DEV_IO_TRACE
unsigned int btrace_seq;
#endif
unsigned int policy;
unsigned long max_allowed_capacity;
int nr_cpus_allowed;
const cpumask_t *cpus_ptr;
cpumask_t *user_cpus_ptr;
cpumask_t cpus_mask;
void *migration_pending;
#ifdef CONFIG_SMP
unsigned short migration_disabled;
#endif
unsigned short migration_flags;
#ifdef CONFIG_PREEMPT_RCU
int rcu_read_lock_nesting;
union rcu_special rcu_read_unlock_special;
struct list_head rcu_node_entry;
struct rcu_node *rcu_blocked_node;
#endif /* #ifdef CONFIG_PREEMPT_RCU */
#ifdef CONFIG_TASKS_RCU
unsigned long rcu_tasks_nvcsw;
u8 rcu_tasks_holdout;
u8 rcu_tasks_idx;
int rcu_tasks_idle_cpu;
struct list_head rcu_tasks_holdout_list;
int rcu_tasks_exit_cpu;
struct list_head rcu_tasks_exit_list;
#endif /* #ifdef CONFIG_TASKS_RCU */
#ifdef CONFIG_TASKS_TRACE_RCU
int trc_reader_nesting;
int trc_ipi_to_cpu;
union rcu_special trc_reader_special;
struct list_head trc_holdout_list;
struct list_head trc_blkd_node;
int trc_blkd_cpu;
#endif /* #ifdef CONFIG_TASKS_TRACE_RCU */
struct sched_info sched_info;
struct list_head tasks;
#ifdef CONFIG_SMP
struct plist_node pushable_tasks;
struct rb_node pushable_dl_tasks;
#endif
struct mm_struct *mm;
struct mm_struct *active_mm;
struct address_space *faults_disabled_mapping;
int exit_state;
int exit_code;
int exit_signal;
/* The signal sent when the parent dies: */
int pdeath_signal;
/* JOBCTL_*, siglock protected: */
unsigned long jobctl;
/* Used for emulating ABI behavior of previous Linux versions: */
unsigned int personality;
/* Scheduler bits, serialized by scheduler locks: */
unsigned sched_reset_on_fork:1;
unsigned sched_contributes_to_load:1;
unsigned sched_migrated:1;
unsigned sched_task_hot:1;
/* Force alignment to the next boundary: */
unsigned :0;
/* Unserialized, strictly 'current' */
/*
* This field must not be in the scheduler word above due to wakelist
* queueing no longer being serialized by p->on_cpu. However:
*
* p->XXX = X; ttwu()
* schedule() if (p->on_rq && ..) // false
* smp_mb__after_spinlock(); if (smp_load_acquire(&p->on_cpu) && //true
* deactivate_task() ttwu_queue_wakelist())
* p->on_rq = 0; p->sched_remote_wakeup = Y;
*
* guarantees all stores of 'current' are visible before
* ->sched_remote_wakeup gets used, so it can be in this word.
*/
unsigned sched_remote_wakeup:1;
#ifdef CONFIG_RT_MUTEXES
unsigned sched_rt_mutex:1;
#endif
/* Bit to tell TOMOYO we're in execve(): */
unsigned in_execve:1;
unsigned in_iowait:1;
#ifndef TIF_RESTORE_SIGMASK
unsigned restore_sigmask:1;
#endif
#ifdef CONFIG_MEMCG_V1
unsigned in_user_fault:1;
#endif
#ifdef CONFIG_LRU_GEN
/* whether the LRU algorithm may apply to this access */
unsigned in_lru_fault:1;
#endif
#ifdef CONFIG_COMPAT_BRK
unsigned brk_randomized:1;
#endif
#ifdef CONFIG_CGROUPS
/* disallow userland-initiated cgroup migration */
unsigned no_cgroup_migration:1;
/* task is frozen/stopped (used by the cgroup freezer) */
unsigned frozen:1;
#endif
#ifdef CONFIG_BLK_CGROUP
unsigned use_memdelay:1;
#endif
#ifdef CONFIG_PSI
/* Stalled due to lack of memory */
unsigned in_memstall:1;
#endif
#ifdef CONFIG_PAGE_OWNER
/* Used by page_owner=on to detect recursion in page tracking. */
unsigned in_page_owner:1;
#endif
#ifdef CONFIG_EVENTFD
/* Recursion prevention for eventfd_signal() */
unsigned in_eventfd:1;
#endif
#ifdef CONFIG_ARCH_HAS_CPU_PASID
unsigned pasid_activated:1;
#endif
#ifdef CONFIG_X86_BUS_LOCK_DETECT
unsigned reported_split_lock:1;
#endif
#ifdef CONFIG_TASK_DELAY_ACCT
/* delay due to memory thrashing */
unsigned in_thrashing:1;
#endif
#ifdef CONFIG_PREEMPT_RT
struct netdev_xmit net_xmit;
#endif
unsigned long atomic_flags; /* Flags requiring atomic access. */
struct restart_block restart_block;
pid_t pid;
pid_t tgid;
#ifdef CONFIG_STACKPROTECTOR
/* Canary value for the -fstack-protector GCC feature: */
unsigned long stack_canary;
#endif
/*
* Pointers to the (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
/* Real parent process: */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
/*
* 'ptraced' is the list of tasks this task is using ptrace() on.
*
* This includes both natural children and PTRACE_ATTACH targets.
* 'ptrace_entry' is this task's link on the p->parent->ptraced list.
*/
struct list_head ptraced;
struct list_head ptrace_entry;
/* PID/PID hash table linkage. */
struct pid *thread_pid;
struct hlist_node pid_links[PIDTYPE_MAX];
struct list_head thread_node;
struct completion *vfork_done;
/* CLONE_CHILD_SETTID: */
int __user *set_child_tid;
/* CLONE_CHILD_CLEARTID: */
int __user *clear_child_tid;
/* PF_KTHREAD | PF_IO_WORKER */
void *worker_private;
u64 utime;
u64 stime;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIME
u64 utimescaled;
u64 stimescaled;
#endif
u64 gtime;
struct prev_cputime prev_cputime;
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
struct vtime vtime;
#endif
#ifdef CONFIG_NO_HZ_FULL
atomic_t tick_dep_mask;
#endif
/* Context switch counts: */
unsigned long nvcsw;
unsigned long nivcsw;
/* Monotonic time in nsecs: */
u64 start_time;
/* Boot based time in nsecs: */
u64 start_boottime;
/* MM fault and swap info: this can arguably be seen as either mm-specific or thread-specific: */
unsigned long min_flt;
unsigned long maj_flt;
/* Empty if CONFIG_POSIX_CPUTIMERS=n */
struct posix_cputimers posix_cputimers;
#ifdef CONFIG_POSIX_CPU_TIMERS_TASK_WORK
struct posix_cputimers_work posix_cputimers_work;
#endif
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif
/*
* executable name, excluding path.
*
* - normally initialized begin_new_exec()
* - set it with set_task_comm()
* - strscpy_pad() to ensure it is always NUL-terminated and
* zero-padded
* - task_lock() to ensure the operation is atomic and the name is
* fully updated.
*/
char comm[TASK_COMM_LEN];
struct nameidata *nameidata;
#ifdef CONFIG_SYSVIPC
struct sysv_sem sysvsem;
struct sysv_shm sysvshm;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
unsigned long last_switch_count;
unsigned long last_switch_time;
#endif
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
#ifdef CONFIG_IO_URING
struct io_uring_task *io_uring;
#endif
/* Namespaces: */
struct nsproxy *nsproxy;
/* Signal handlers: */
struct signal_struct *signal;
struct sighand_struct __rcu *sighand;
sigset_t blocked;
sigset_t real_blocked;
/* Restored if set_restore_sigmask() was used: */
sigset_t saved_sigmask;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
unsigned int sas_ss_flags;
struct callback_head *task_works;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_AUDITSYSCALL
struct audit_context *audit_context;
#endif
kuid_t loginuid;
unsigned int sessionid;
#endif
struct seccomp seccomp;
struct syscall_user_dispatch syscall_dispatch;
/* Thread group tracking: */
u64 parent_exec_id;
u64 self_exec_id;
/* Protection against (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;
struct wake_q_node wake_q;
#ifdef CONFIG_RT_MUTEXES
/* PI waiters blocked on a rt_mutex held by this task: */
struct rb_root_cached pi_waiters;
/* Updated under owner's pi_lock and rq lock */
struct task_struct *pi_top_task;
/* Deadlock detection and priority inheritance handling: */
struct rt_mutex_waiter *pi_blocked_on;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
/* Mutex deadlock detection: */
struct mutex_waiter *blocked_on;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK_BLOCKER
struct mutex *blocker_mutex;
#endif
#ifdef CONFIG_DEBUG_ATOMIC_SLEEP
int non_block_count;
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
struct irqtrace_events irqtrace;
unsigned int hardirq_threaded;
u64 hardirq_chain_key;
int softirqs_enabled;
int softirq_context;
int irq_config;
#endif
#ifdef CONFIG_PREEMPT_RT
int softirq_disable_cnt;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48UL
u64 curr_chain_key;
int lockdep_depth;
unsigned int lockdep_recursion;
struct held_lock held_locks[MAX_LOCK_DEPTH];
#endif
#if defined(CONFIG_UBSAN) && !defined(CONFIG_UBSAN_TRAP)
unsigned int in_ubsan;
#endif
/* Journalling filesystem info: */
void *journal_info;
/* Stacked block device info: */
struct bio_list *bio_list;
/* Stack plugging: */
struct blk_plug *plug;
/* VM state: */
struct reclaim_state *reclaim_state;
struct io_context *io_context;
#ifdef CONFIG_COMPACTION
struct capture_control *capture_control;
#endif
/* Ptrace state: */
unsigned long ptrace_message;
kernel_siginfo_t *last_siginfo;
struct task_io_accounting ioac;
#ifdef CONFIG_PSI
/* Pressure stall state */
unsigned int psi_flags;
#endif
#ifdef CONFIG_TASK_XACCT
/* Accumulated RSS usage: */
u64 acct_rss_mem1;
/* Accumulated virtual memory usage: */
u64 acct_vm_mem1;
/* stime + utime since last update: */
u64 acct_timexpd;
#endif
#ifdef CONFIG_CPUSETS
/* Protected by ->alloc_lock: */
nodemask_t mems_allowed;
/* Sequence number to catch updates: */
seqcount_spinlock_t mems_allowed_seq;
int cpuset_mem_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS
/* Control Group info protected by css_set_lock: */
struct css_set __rcu *cgroups;
/* cg_list protected by css_set_lock and tsk->alloc_lock: */
struct list_head cg_list;
#endif
#ifdef CONFIG_X86_CPU_RESCTRL
u32 closid;
u32 rmid;
#endif
#ifdef CONFIG_FUTEX
struct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPAT
struct compat_robust_list_head __user *compat_robust_list;
#endif
struct list_head pi_state_list;
struct futex_pi_state *pi_state_cache;
struct mutex futex_exit_mutex;
unsigned int futex_state;
#endif
#ifdef CONFIG_PERF_EVENTS
u8 perf_recursion[PERF_NR_CONTEXTS];
struct perf_event_context *perf_event_ctxp;
struct mutex perf_event_mutex;
struct list_head perf_event_list;
struct perf_ctx_data __rcu *perf_ctx_data;
#endif
#ifdef CONFIG_DEBUG_PREEMPT
unsigned long preempt_disable_ip;
#endif
#ifdef CONFIG_NUMA
/* Protected by alloc_lock: */
struct mempolicy *mempolicy;
short il_prev;
u8 il_weight;
short pref_node_fork;
#endif
#ifdef CONFIG_NUMA_BALANCING
int numa_scan_seq;
unsigned int numa_scan_period;
unsigned int numa_scan_period_max;
int numa_preferred_nid;
unsigned long numa_migrate_retry;
/* Migration stamp: */
u64 node_stamp;
u64 last_task_numa_placement;
u64 last_sum_exec_runtime;
struct callback_head numa_work;
/*
* This pointer is only modified for current in syscall and
* pagefault context (and for tasks being destroyed), so it can be read
* from any of the following contexts:
* - RCU read-side critical section
* - current->numa_group from everywhere
* - task's runqueue locked, task not running
*/
struct numa_group __rcu *numa_group;
/*
* numa_faults is an array split into four regions:
* faults_memory, faults_cpu, faults_memory_buffer, faults_cpu_buffer
* in this precise order.
*
* faults_memory: Exponential decaying average of faults on a per-node
* basis. Scheduling placement decisions are made based on these
* counts. The values remain static for the duration of a PTE scan.
* faults_cpu: Track the nodes the process was running on when a NUMA
* hinting fault was incurred.
* faults_memory_buffer and faults_cpu_buffer: Record faults per node
* during the current scan window. When the scan completes, the counts
* in faults_memory and faults_cpu decay and these values are copied.
*/
unsigned long *numa_faults;
unsigned long total_numa_faults;
/*
* numa_faults_locality tracks if faults recorded during the last
* scan window were remote/local or failed to migrate. The task scan
* period is adapted based on the locality of the faults with different
* weights depending on whether they were shared or private faults
*/
unsigned long numa_faults_locality[3];
unsigned long numa_pages_migrated;
#endif /* CONFIG_NUMA_BALANCING */
#ifdef CONFIG_RSEQ
struct rseq __user *rseq;
u32 rseq_len;
u32 rseq_sig;
/*
* RmW on rseq_event_mask must be performed atomically
* with respect to preemption.
*/
unsigned long rseq_event_mask;
# ifdef CONFIG_DEBUG_RSEQ
/*
* This is a place holder to save a copy of the rseq fields for
* validation of read-only fields. The struct rseq has a
* variable-length array at the end, so it cannot be used
* directly. Reserve a size large enough for the known fields.
*/
char rseq_fields[sizeof(struct rseq)];
# endif
#endif
#ifdef CONFIG_SCHED_MM_CID
int mm_cid; /* Current cid in mm */
int last_mm_cid; /* Most recent cid in mm */
int migrate_from_cpu;
int mm_cid_active; /* Whether cid bitmap is active */
struct callback_head cid_work;
#endif
struct tlbflush_unmap_batch tlb_ubc;
/* Cache last used pipe for splice(): */
struct pipe_inode_info *splice_pipe;
struct page_frag task_frag;
#ifdef CONFIG_TASK_DELAY_ACCT
struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
int make_it_fail;
unsigned int fail_nth;
#endif
/*
* When (nr_dirtied >= nr_dirtied_pause), it's time to call
* balance_dirty_pages() for a dirty throttling pause:
*/
int nr_dirtied;
int nr_dirtied_pause;
/* Start of a write-and-pause period: */
unsigned long dirty_paused_when;
#ifdef CONFIG_LATENCYTOP
int latency_record_count;
struct latency_record latency_record[LT_SAVECOUNT];
#endif
/*
* Time slack values; these are used to round up poll() and
* select() etc timeout values. These are in nanoseconds.
*/
u64 timer_slack_ns;
u64 default_timer_slack_ns;
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
unsigned int kasan_depth;
#endif
#ifdef CONFIG_KCSAN
struct kcsan_ctx kcsan_ctx;
#ifdef CONFIG_TRACE_IRQFLAGS
struct irqtrace_events kcsan_save_irqtrace;
#endif
#ifdef CONFIG_KCSAN_WEAK_MEMORY
int kcsan_stack_depth;
#endif
#endif
#ifdef CONFIG_KMSAN
struct kmsan_ctx kmsan_ctx;
#endif
#if IS_ENABLED(CONFIG_KUNIT)
struct kunit *kunit_test;
#endif
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
/* Index of current stored address in ret_stack: */
int curr_ret_stack;
int curr_ret_depth;
/* Stack of return addresses for return function tracing: */
unsigned long *ret_stack;
/* Timestamp for last schedule: */
unsigned long long ftrace_timestamp;
unsigned long long ftrace_sleeptime;
/*
* Number of functions that haven't been traced
* because of depth overrun:
*/
atomic_t trace_overrun;
/* Pause tracing: */
atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING
/* Bitmask and counter of trace recursion: */
unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
#ifdef CONFIG_KCOV
/* See kernel/kcov.c for more details. */
/* Coverage collection mode enabled for this task (0 if disabled): */
unsigned int kcov_mode;
/* Size of the kcov_area: */
unsigned int kcov_size;
/* Buffer for coverage collection: */
void *kcov_area;
/* KCOV descriptor wired with this task or NULL: */
struct kcov *kcov;
/* KCOV common handle for remote coverage collection: */
u64 kcov_handle;
/* KCOV sequence number: */
int kcov_sequence;
/* Collect coverage from softirq context: */
unsigned int kcov_softirq;
#endif
#ifdef CONFIG_MEMCG_V1
struct mem_cgroup *memcg_in_oom;
#endif
#ifdef CONFIG_MEMCG
/* Number of pages to reclaim on returning to userland: */
unsigned int memcg_nr_pages_over_high;
/* Used by memcontrol for targeted memcg charge: */
struct mem_cgroup *active_memcg;
/* Cache for current->cgroups->memcg->objcg lookups: */
struct obj_cgroup *objcg;
#endif
#ifdef CONFIG_BLK_CGROUP
struct gendisk *throttle_disk;
#endif
#ifdef CONFIG_UPROBES
struct uprobe_task *utask;
#endif
#if defined(CONFIG_BCACHE) || defined(CONFIG_BCACHE_MODULE)
unsigned int sequential_io;
unsigned int sequential_io_avg;
#endif
struct kmap_ctrl kmap_ctrl;
#ifdef CONFIG_DEBUG_ATOMIC_SLEEP
unsigned long task_state_change;
# ifdef CONFIG_PREEMPT_RT
unsigned long saved_state_change;
# endif
#endif
struct rcu_head rcu;
refcount_t rcu_users;
int pagefault_disabled;
#ifdef CONFIG_MMU
struct task_struct *oom_reaper_list;
struct timer_list oom_reaper_timer;
#endif
#ifdef CONFIG_VMAP_STACK
struct vm_struct *stack_vm_area;
#endif
#ifdef CONFIG_THREAD_INFO_IN_TASK
/* A live task holds one reference: */
refcount_t stack_refcount;
#endif
#ifdef CONFIG_LIVEPATCH
int patch_state;
#endif
#ifdef CONFIG_SECURITY
/* Used by LSM modules for access restriction: */
void *security;
#endif
#ifdef CONFIG_BPF_SYSCALL
/* Used by BPF task local storage */
struct bpf_local_storage __rcu *bpf_storage;
/* Used for BPF run context */
struct bpf_run_ctx *bpf_ctx;
#endif
/* Used by BPF for per-TASK xdp storage */
struct bpf_net_context *bpf_net_context;
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
unsigned long lowest_stack;
unsigned long prev_lowest_stack;
#endif
#ifdef CONFIG_X86_MCE
void __user *mce_vaddr;
__u64 mce_kflags;
u64 mce_addr;
__u64 mce_ripv : 1,
mce_whole_page : 1,
__mce_reserved : 62;
struct callback_head mce_kill_me;
int mce_count;
#endif
#ifdef CONFIG_KRETPROBES
struct llist_head kretprobe_instances;
#endif
#ifdef CONFIG_RETHOOK
struct llist_head rethooks;
#endif
#ifdef CONFIG_ARCH_HAS_PARANOID_L1D_FLUSH
/*
* If L1D flush is supported on mm context switch
* then we use this callback head to queue kill work
* to kill tasks that are not running on SMT disabled
* cores
*/
struct callback_head l1d_flush_kill;
#endif
#ifdef CONFIG_RV
/*
* Per-task RV monitor. Nowadays fixed in RV_PER_TASK_MONITORS.
* If we find justification for more monitors, we can think
* about adding more or developing a dynamic method. So far,
* none of these are justified.
*/
union rv_task_monitor rv[RV_PER_TASK_MONITORS];
#endif
#ifdef CONFIG_USER_EVENTS
struct user_event_mm *user_event_mm;
#endif
/*
* New fields for task_struct should be added above here, so that
* they are included in the randomized portion of task_struct.
*/
randomized_struct_fields_end
/* CPU-specific state of this task: */
struct thread_struct thread;
/*
* WARNING: on x86, 'thread_struct' contains a variable-sized
* structure. It *MUST* be at the end of 'task_struct'.
*
* Do not put anything below here!
*/
};
4. 进程内核栈(Kernel Stack)
每个进程最终都会通过系统调用陷入内核执行,进程内核栈用于在执行系统调用、异常或中断处理时保存上下文。在内核态运行的进程所使用的栈会与用户空间的栈不同,是一个在单独在内核空间的栈,也被称作内核栈。进程内核栈在进程创建的时候调用 alloc_thread_stack_node
函数最终由 __slab_alloc_node
函数从 slab
缓存池中分配内存,其大小为 THREAD_SIZE
。从历史上说,通常来讲内核栈的大小是两页,也就是在 32
位机器上的内核栈就是 8KB
,64
位机器上的内核栈就是 16KB
,是一个固定不变的值。
[注]:有关内核栈分配与管理的相关内容笔者将在后面的文章中继续与大家一同学习,本文暂不涉及改内容。
4.1. 进程内核栈的定义
内核栈的定义如下,内核栈在理论上被定义为一个 union
,该结构被称之为联合体或者共用体,该类型的特性是 “一个联合体的大小等于其内部所占空间最大的成员大小,并且所有成员都共享同一段内存”。根据这个特性,我们也就知道了一个内核栈 thread_union
的大小就是 sizeof(unsigned long) * (THREAD_SIZE / sizeof(long)) = THERAD_SIZE
。
// Linux Kernel 2.6.34
// PATH: include/linux/sched.h
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
//--------------------------------------------------------------------
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched.h
union thread_union {
struct task_struct task;
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
为什么这里笔者要强调 “理论上” 一词,这是由于在以往较旧版本的内核中的确是这样做的,在网络上有很多博客和文章都在介绍内核栈也是用的较老版本的内核代码,因此他们的说法也是对的。较老版本的内核中的内核栈是将 thread_info
与 真正的内核栈 stack
绑定在一起通过 alloc_thread_info
分配两个页面的内存空间。因此,这时候的 thread_info
地址就是内核栈的内存起始地址。(在旧版本的内核中) 而在 thread_info
中定义了指向进程描述符 struct_task
的指针,以此来寻找进程描述符。
// Linux Kernel 2.6.34
// PATH: arch/x86/include/asm/thread_info.h
#define alloc_thread_info(tsk) \
((struct thread_info *)__get_free_pages(THREAD_FLAGS, THREAD_ORDER))
__get_free_pages
是一个内核函数,用于分配连续的物理内存页。THREAD_ORDER
决定了内核栈的大小(通常为2
个页)。- 分配的内存同时包含
thread_info
和内核栈,thread_info
位于内核栈的底部。
因此,旧版本的内核栈就看起来像如下图这两种都是对的。
[注]:x86 的栈是向下生长的,即从高地址向低地址扩展。
在新版本中的内核代码中 thread_union
的定义仍然存在,但它很少被直接使用。这是因为内核的设计已经逐渐从直接依赖 thread_union
转向更灵活的方式来管理任务的内核栈和相关数据结构,例如通过 task_struct
和独立的栈分配机制,在 dup_task_struct
中使用 alloc_thread_stack_node
来为 task_struct
分配内存空间,使用 alloc_thread_stack_node
为内核栈分配内存空间。因此新版本的内核栈看起来就像是这样。也可以简单的认为内核栈在逻辑上就是一片连续的内存空间。
内核栈空间是否物理连续取决于
CONFIG_VMAP_STACK
是否开启,开启则内核栈空间最终由__vmalloc_node
使用虚拟内存分配,虽然虚拟地址是连续的,但物理上可能是分散的。反之则最终通过alloc_pages_node
分配的内存是物理连续的。(一般情况,该配置是默认开启。)
[注]:这里实际上已经和 thread_union 没什么太大关系了。
由于笔者是在
x86_64
的机器下编写本文。因此这里主要就以x86_64
架构为主,不同架构在arch
目录下的代码会有不同,日后有余力在将其整理对比。下面就来看看THREAD_SIZE
到底是多少。
找到 x86
架构下的 THREAD_SIZE
定义位置,如下。可以看到 THREAD_SIZE
是通过 PAGE_SIZE
来计算的。在 32
位下,直接就等于 PAGE_SIZE * 2
,而在 64
位下则需要判断 CONFIG_KASAN
,然后再进行计算。
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/include/asm/page_32_types.h
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
//--------------------------------------------------------------
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/include/asm/page_64_types.h
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
再来找到 PAGE_SIZE
的定义位置,直接就是通过 1 << CONFIG_PAGE_SHIFT
这个值来得到最终页面大小。
// Linux Kernel 6.15.0-rc2
// PATH: include/vdso/page.h
#define PAGE_SHIFT CONFIG_PAGE_SHIFT
#define PAGE_SIZE (_AC(1,UL) << CONFIG_PAGE_SHIFT)
通过查看编译内核时的 .config
文件就可以这两个未确定的值是多少,那么这里从而也就确定了。其实此时的 PAGE_SIZE
都是 1 << 12 = 4096 = 4KB
,这里可以看到在 32
位机器上,那么 THREAD_SIZE = 2 * 4KB = 8KB
。而在 64
位机器上,就是 THREAD_SIZE = 2^2 * 4KB = 16KB
。#02
[注]:具体验证计算结果的过程可见文末附 #02 部分内容。
4.2. thread_info 体系结构相关进程描述
在上面小节中内核栈的定义代码里我们可以看到除了 task_struct
类型,还有一个不可忽视的 thread_info
类型。通过代码了解到 thread_info
在 thread_union
和 task_struct
中都可能包含。
[注]:虽然笔者已经提前告知各位 thread_union 类型几乎没什么作用了,不过为了研究透彻,这里我们还是抱着学习的心态一起探个究竟。
// Linux Kernel 2.6.34
// PATH: arch/x86/include/asm/thread_info.h
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable,
<0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack in
case of nested (IRQ) stacks
*/
__u8 supervisor_stack[0];
#endif
int uaccess_err;
};
//--------------------------------------------------------------------
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/include/asm/thread_info.h
struct thread_info {
unsigned long flags; /* low level flags */
unsigned long syscall_work; /* SYSCALL_WORK_ flags */
u32 status; /* thread synchronous flags */
#ifdef CONFIG_SMP
u32 cpu; /* current CPU */
#endif
};
实际上在 linux kernel
中,task_struct
、thread_info
都用来保存进程相关信息,即进程 PCB(Process Control Block)
信息。然而不同的体系结构里,进程需要存储的信息不尽相同,因此 linux
将其分为两部分存储,使用 task_struct
存储进程通用的信息,将体系结构相关的部分存储在 thread_info
中。这也是为什么 struct task_struct
在 include/linux/sched.h
中定义,而 thread_info
在 arch/
下体系结构相关头文件里定义的原因。下面具体看看 thread_info
到底会被包含在哪个类型中。
在代码中以及 .config
文件中我们了解到,CONFIG_THREAD_INFO_IN_TASK
这个宏有定义(事实上这个宏 CONFIG_THREAD_INFO_IN_TASK
在新版内核 x86_64
中是默认开启的),那么当前内核就是直接把 thread_info
放进 task_struct
里,则 thread_union
中不再包含,反之则反。也就是说进程内核栈 thread_union
的首地址就是 task_struct
的首地址,也就是 task_struct
的第一个成员 thread_info
的地址。 那么整个内核栈空间目前就看起来像下图这样。
[注]:请注意标黄这段分析,这是在假设现在还不清楚内核是否不再使用 thread_union 的情况。这里假设使用的就是 thread_union,则符合此分析结果。
[注]:x86 的栈是向下生长的,即从高地址向低地址扩展。
4.3. 定位进程描述符(task_struct)和内核栈以及内核栈指针的问题
通过以上内容我们已经了解到了一个内核栈的具体样貌。接下来一起来学习如何定位一个内核栈和进程描述符(task_struct
也就是 PCB
),或者说是,用代码如何找到进程的内核栈(kernel stack
)以及一个进程的进程描述符(task_struct
)。
通过上文我们已经知道了有一个配置宏是默认开启的,那就是 CONFIG_THREAD_INFO_IN_TASK
,与它相关的代码还有下面这部分。
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/thread_info.h
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For CONFIG_THREAD_INFO_IN_TASK kernels we need <asm/current.h> for the
* definition of current, but for !CONFIG_THREAD_INFO_IN_TASK kernels,
* including <asm/current.h> can cause a circular dependency on some platforms.
*/
#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)
#endif
其实通过函数名字就可以才得到它的意图,就是获取 thread_info
,而 thread_info
的首地址也就等于 task_struct
首地址。无论是通过所包含的头文件,还是通过注释内容我们都知道,这里用来定义宏的 current
应该去体系结构相关的头文件下去找 current.h
。
// Linux Kernel 6.15.0-rc2
// arch/x86/include/asm/current.h
struct task_struct;
DECLARE_PER_CPU_CACHE_HOT(struct task_struct *, current_task);
/* const-qualified alias provided by the linker. */
DECLARE_PER_CPU_CACHE_HOT(struct task_struct * const __percpu_seg_override,
const_current_task);
static __always_inline struct task_struct *get_current(void)
{
if (IS_ENABLED(CONFIG_USE_X86_SEG_SUPPORT))
return this_cpu_read_const(const_current_task);
return this_cpu_read_stable(current_task);
}
#define current get_current()
那么这里就来到了 x86
架构下的 current.h
文件,current
是一个宏定义,最终调用的是 get_current
函数来获取当前函数的 task_struct
指针。
get_current
函数涉及的内容较多,这里简单介绍一下get_currnet
函数的工作原理。
这里使用到了内核的一个per-CPU
机制来获取当前进程的task_struct
。per-CPU
变量是一种特殊的变量类型,每个CPU
都有自己独立的实例。这种机制允许每个CPU
独立访问和操作自己的变量实例。每个 CPU 分配独立存储空间的变量,每个 CPU 都有自己的副本。
- 当前代码中首先用
DECLARE_PER_CPU_CACHE_HOT
声明了一个per-CPU
变量,类型为struct task_struct *
;this_cpu_read_stable
是一个内核宏,用于读取当前CPU
上per-CPU
变量的current_task
值并以struct task_struct *
类型返回。到此就完成了读取当前进程
task_struct
的操作。
Linux
内核引入per-CPU
变量之后,逐渐通过per-CPU
变量来实现current
宏,x86
从Linux Kernel 4.1
版本开始逐渐简化thread_info
结构体,直到Linux Kernel 4.9
便彻底移除了thread_info
中的task
,也就不再通过thread_info
获取task_struct
指针了,而改用current_struct percpu
变量存放task_struct
指针,详情可参阅该 [PATCH] x86: Move thread_info into task_struct。
那么这里其实就已经有办法获取当前进程的 task_struct
值了,而根据上面黄标的分析内容,进程描述符 task_struct
的第一个成员就是 thread_info
,而 thread_union
的第一个成员也是 task_struct
,因此实际上在当前内核中,这三个地址是相同的,这里将会是得到一个这样的现象。那么这里用代码来验证一下我们的假设。#03
[72607.977772] task_struct address: ffff88fd9520a940
[72607.977773] task_struct->stack(stack base): ffffca988fdb0000
[72607.977774] task_struct->thread_info address: ffff88fd9520a940
[注]:为了提升本文阅读效果,此处仅展示验证结果,具体过程见文末附 #03 小节内容。
这里发现三个地址并不相同,而只有 task_struct
与 thread_info
是相同的,这是由于 thread_info
就是 task_struct
第一个成员。那么这里也就是验证了其实内核栈地址并不是如同 thread_union
描述的那样。那么通过这里我们更加确信了内核并不是使用 thread_union
这一类型来描述内核栈的,实际上 task_struct
与内核栈是独立分配的,因此在内核栈内存中与 task_struct
的关系应该如下图一般。也可以简单的将内核栈认为是一个逻辑上连续的内存空间。
可以看到上图中笔者还标注了 sp
的位置,这正是我们所熟知的栈指针,而在 task_struct
中的 *stack
所指向的位置并非程序意义上的栈顶或栈底位置,而是是内核栈的内存起始地址。由于 x86
栈的生长方向是向下的,因此栈底位置应该是 task_struct->stack + THREAD_SIZE
,而栈顶的位置保存在 sp
寄存器中,在 x86_64
就是 rsp
寄存器中。以下是验证结果。#04
[73502.218940] task_struct->stack(stack base): ffffca9889dc4000
[73502.218942] task_struct->stack(stack top): ffffca9889de4000
[73502.218944] Current stack pointer (sp): ffffca9889dc77d0
[注]:此处同样仅展示结果内容,具体验证代码见文末附 #04 小节。
5. 进程 ID(PID)
内核通过一个唯一的进程 ID
即 PID(Process identification)
来标识每个进程。内核把每个进程的 PID
存放在他们各自的进程描述符中(task_struct
)的 pid
字段中。
进程 PID
为了与老版本的 Unix
和 Linux
兼容,其最大值默认是 32768(0x8000)
(short int
短整型的最大值),该限制在 <linux/thread.h>
中定义。不过在某些大型系统确实需要更多进程数的话,也可以不考虑兼容问题,直接通过修改 /proc/sys/kernel/pic_max
来提高上限。
// Linux Kernel 2.6.34
// Linux Kernel 6.15.0-rc2
// PATH: linux/include/linux/threads.h
/*
* This controls the default maximum pid allocated to a process
*/
#define PID_MAX_DEFAULT (IS_ENABLED(CONFIG_BASE_SMALL) ? 0x1000 : 0x8000)
6. 进程状态
6.1. 进程的几个状态
进程状态的管理和切换主要通过 task_struct
结构体中的 __state
字段以及相关的状态宏和函数实现。
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched.h
/* Used in tsk->__state: */
#define TASK_RUNNING 0x00000000
#define TASK_INTERRUPTIBLE 0x00000001
#define TASK_UNINTERRUPTIBLE 0x00000002
#define __TASK_STOPPED 0x00000004
#define __TASK_TRACED 0x00000008
#define TASK_DEAD 0x00000080
#define TASK_WAKEKILL 0x00000100
#define TASK_WAKING 0x00000200
#define TASK_NOLOAD 0x00000400
#define TASK_NEW 0x00000800
#define TASK_RTLOCK_WAIT 0x00001000
-
TASK_RUNNING:进程为可执行状态(准备运行),或者是正在执行中,或者是在运行队列中等待执行;
-
TASK_INTERRUPTIBLE:进程可中断,表示进程正在阻塞,等待某些条件完成(并且可以被信号中断)。一旦这些条件完成,内核就会把进程设置为运行态。该状态下的进程也可接收信号被提前唤醒准备投入运行,进程在等待时会主动调用
set_current_state(TASK_INTERRUPTIBLE)
; -
TASK_UNINTERRUPTIBLE:进程不可被中断,也就是接收到信号也不会提前唤醒进入运行,而是继续阻塞等待条件完成。适用于需要确保操作完整性的场景(如设备驱动程序中的关键操作)。同时由于此状态的任务对信号不做响应,所以该状态使用的情况较少(此状态下的进程——执行
ps
命令是状态为D
的进程,是无法被kill
命令直接杀死,由于该任务不响应信号。); -
__TASK_STOPPED:进程停止执行,进程没有投入运行,也不能被投入运行。通常这种状态发生在接收到
SIGSTOP
、SIGTSTP
、SIGTTIN
、SIGTTOU
等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态(进程可以通过SIGCONT
信号恢复运行,状态切换为TASK_RUNNING
); -
__TASK_TRACED:被其它进程跟踪的进程(例如通过
ptrace
、gdb
对调试程序进行跟踪); -
TASK_DEAD:表示进程已经退出,等待被回收。进程在调用
do_exit()
后进入此状态; -
TASK_WAKEKILL:表示进程可以被信号唤醒,即使它处于不可中断的等待状态。通常用于需要强制唤醒的场景;
-
TASK_WAKING:表示进程正在从等待状态切换到运行状态。这是一个过渡状态,通常由调度器内部使用;
-
TASK_NOLOAD:表示进程不会对系统的负载统计产生影响。通常用于内核线程或其他特殊任务;
-
TASK_NEW:表示新创建的任务,尚未被调度运行。通常在任务初始化阶段使用;
-
TASK_RTLOCK_WAIT:表示进程正在等待实时锁(
RT lock
)。这是一个特殊状态,用于实时调度场景; -
TASK_FREEZABLE:表示进程可以被冻结(如在系统挂起时)。进程在冻结时会暂停运行,直到系统恢复;
-
TASK_FROZEN:表示进程已被冻结。通常用于系统挂起或休眠操作;
进程的几个退出状态:
-
EXIT_DEAD:表示进程已经完全退出,等待被回收。与
TASK_DEAD
类似,但用于exit_state
字段; -
EXIT_ZOMBIE:表示进程已退出,但其父进程尚未调用
wait()
回收其状态。进程在此状态下被称为“僵尸进程”; -
TASK_IDLE:表示进程处于空闲状态,不会对系统负载产生影响。通常用于 CPU 的空闲任务。
6.2. 设置进程状态
内核提供了多个宏和函数来设置或修改进程状态:
设置当前状态
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched.h
#define __set_current_state(state_value) \
do { \
debug_normal_state_change((state_value)); \
trace_set_current_state(state_value); \
WRITE_ONCE(current->__state, (state_value)); \
} while (0)
#define set_current_state(state_value) \
do { \
debug_normal_state_change((state_value)); \
trace_set_current_state(state_value); \
smp_store_mb(current->__state, (state_value)); \
} while (0)
__set_current_state
:直接设置当前任务的状态。set_current_state
:在设置状态时添加内存屏障以确保状态的正确性。
设置特殊状态
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched.h
#define set_special_state(state_value) \
do { \
unsigned long flags; \
raw_spin_lock_irqsave(¤t->pi_lock, flags);\
debug_special_state_change((state_value)); \
trace_set_current_state(state_value); \
WRITE_ONCE(current->__state, (state_value)); \
raw_spin_unlock_irqrestore(¤t->pi_lock, flags);\
} while (0)
set_special_state
:用于设置特殊状态(如TASK_DEAD
),并确保与唤醒操作的同步。
6.3. 进程状态检查
通过以下宏检查任务的状态:
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched.h
#define task_is_running(task) (READ_ONCE((task)->__state) == TASK_RUNNING)
#define task_is_traced(task) ((READ_ONCE(task->jobctl) & JOBCTL_TRACED) != 0)
#define task_is_stopped(task) ((READ_ONCE(task->jobctl) & JOBCTL_STOPPED) != 0)
6.4. 状态切换的几个核心函数
- schedule()
schedule()
是内核中用于调度任务的核心函数。它会根据任务的状态和优先级选择下一个要运行的任务。
PATH: kernel/sched/core.c
- try_to_wake_up() / wake_up_process()
try_to_wake_up()
用于唤醒处于非运行状态的任务,并将其状态设置为TASK_RUNNING
。(wake_up_process()
内部也是调用try_to_wake_up()
)
PATH: kernel/sched/core.c
- schedule_timeout()
schedule_timeout()
用于在超时后切换任务状态:
PATH: kernel/time/sleep_timeout.c
6.5. 经典的进程状态转换
通过以上这些机制,内核能够高效地管理和切换进程状态。
这里提到了一个进程上下文的概念,上下文简单说来就是一个环境,相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。当发生进程调度时,进行进程切换就是上下文切换(context switch
)。操作系统必须对上面提到的进程全部信息进行切换,新调度的进程才能运行。
7. 进程创建
7.1. 进程之间的关系
Unix
系统的进程之间存在一个明显的继承关系,在 Linux
系统中也是如此。所有的进程都是 PID
为 1
的 init
进程的后代#05(关于这一点笔者将会在文末对其说明和验证)。内核在系统启动的最后阶段启动 init
进程。该进程读取系统的初始化脚本(initscript
)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个 task_struct
都包含一个指向其父进程 tast_struct
、叫做 parent
的指针,还包含一个称为 children
的子进程链表。
Unix
的进程创建很特别。许多其他的操作系统都提供了产生(spawn
)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix
采用了与众不同的实现方式它把上述步骤分解到两个单独的函数中去执行:fork()
和 exec()
。首先,fork()
通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于 PID
(每个进程唯一)、PPID
(父进程的进程号,子进程将其设置为被拷贝进程的 PID
)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()
)函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。
[注]:这里的 exec() 所指的是 exec 一族函数。
7.2. 进程创建的写时拷贝机制(copy-on-write)
Linux
的 fork()
使用写时拷贝(copy-on-write
)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个内存空间。只有在需要写人的时候,数据才会被复制,从而使各个进程拥有各自的内存空间。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(举例来说 fork()
后立即调用 exec()
) 它们就无须复制了。
fork()
的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于 Unix
强调进程快速执行的能力,所以这个优化是很重要的。
在 Linux
内核中,fork()
是一个用于创建新进程的系统调用。相信大家在平时的练习中应该也使用过 fork()
函数来创建一个子进程,通过其返回值来判断是子进程还是父进程,0
为子进程,大于零的值就是父进程(有关 fork()
是如何让用户程序看起来是获取到两个不同的返回值这一现象,笔者将会在后文解释)。它的实现涉及多个子系统,包括进程管理、内存管理和调度器。而本文仅涉及进程管理的内容,因此并不算完全分析该函数,在后面的文章中笔者将会和大家一同更加深入的学习完全透彻的理解整个过创建进程的所有细节过程。
7.3. fork() 函数的入口
fork()
是通过系统调用号进入内核的,其定义如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
struct kernel_clone_args args = {
.exit_signal = SIGCHLD,
};
return kernel_clone(&args);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
-
SYSCALL_DEFINE0
:- 宏
SYSCALL_DEFINE0
用于定义没有参数的系统调用。 - 它会展开为一个函数定义,并将系统调用注册到内核的系统调用表中。
- 宏
-
fork
的实现:- 如果启用了内存管理单元(
CONFIG_MMU
),fork
调用kernel_clone
函数。 kernel_clone
是实际创建新进程的核心函数。SIGCHLD
表示子进程退出时会向父进程发送SIGCHLD
信号。
- 如果启用了内存管理单元(
-
CONFIG_MMU
检查:- 如果系统不支持内存管理单元(如某些嵌入式系统),
fork
返回-EINVAL
,表示不支持。
- 如果系统不支持内存管理单元(如某些嵌入式系统),
这里提到了系统调用号的概念,我们知道所有的用户进程都是通过系统调用陷入内核态执行,而操作系统则是通过系统调用号来区分当前所调用的是哪个系统调用函数,x86_64
系统的系统调用号定义在 arch/x86/entry/syscalls/syscall_64.tbl
文件中,其中 fork()
函数的调用号是 57
。
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/entry/syscalls/syscall_64.tbl
57 common fork sys_fork
7.4. 创建新进程的核心函数 kernel_clone()
从 fork()
函数的定义可以看出该函数的核心内容就是 kernel_clone()
。kernel_clone
是 Linux 内核中用于实现 fork
、vfork
和 clone
系统调用的核心函数。它负责根据传入的参数创建一个新的进程或线程,并完成必要的初始化工作。kernel_clone()
的函数定义如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
pid_t kernel_clone(struct kernel_clone_args *args)
{
u64 clone_flags = args->flags;
struct completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
pid_t nr;
/*
* For legacy clone() calls, CLONE_PIDFD uses the parent_tid argument
* to return the pidfd. Hence, CLONE_PIDFD and CLONE_PARENT_SETTID are
* mutually exclusive. With clone3() CLONE_PIDFD has grown a separate
* field in struct clone_args and it still doesn't make sense to have
* them both point at the same memory location. Performing this check
* here has the advantage that we don't need to have a separate helper
* to check for legacy clone().
*/
if ((clone_flags & CLONE_PIDFD) &&
(clone_flags & CLONE_PARENT_SETTID) &&
(args->pidfd == args->parent_tid))
return -EINVAL;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if (args->exit_signal != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, args->parent_tid);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
if (IS_ENABLED(CONFIG_LRU_GEN_WALKS_MMU) && !(clone_flags & CLONE_VM)) {
/* lock the task to synchronize with memcg migration */
task_lock(p);
lru_gen_add_mm(p->mm);
task_unlock(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
return nr;
}
-
参数:
args
:包含创建新进程所需的参数,类型为struct kernel_clone_args
。flags
:控制新进程的行为(如是否共享内存、文件描述符等)。exit_signal
:子进程退出时发送给父进程的信号。- 其他字段如
stack
、tls
、pidfd
等,用于指定新进程的栈、线程本地存储等。
-
返回值:
- 成功时返回新进程的
PID
。 - 失败时返回负的错误码。
- 成功时返回新进程的
kernel_clone 函数的主要逻辑如下:
7.4.1. 检查参数并调用 copy_process 创建并复制进程
函数开始先对标志位进行检查,检查 CLONE_PIDFD
和 CLONE_PARENT_SETTID
是否同时设置,并确保它们没有指向相同的内存位置。如果条件不满足,返回 -EINVAL
。如果未设置 CLONE_UNTRACED
,根据 flags
确定是否需要报告 ptrace
事件。可能的事件包括 PTRACE_EVENT_FORK
、PTRACE_EVENT_CLONE
和 PTRACE_EVENT_VFORK
。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
接下来调用核心函数 copy_process()
创建新进程,调用 copy_process
函数复制当前进程,创建一个新的 task_struct
。如果 copy_process
失败,返回错误码。
7.4.2. 获取新进程 PID 作为返回值
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, args->parent_tid);
get_task_pid
的作用是为新创建的任务/进程(task_struct
)获取其对应的 pid
结构体。此处的 pid
并非一个确切的数值,而是一个用来描述 pid
的结构体,该类型被定义在 include/linux/pid.h
。get_task_pid
函数的主要作用如下:
-
获取任务的
pid
结构体:- 每个任务(进程或线程)在内核中都有一个唯一的
pid
结构体,用于表示其在特定PID
命名空间中的标识符。 get_task_pid
会根据任务的类型(如PIDTYPE_PID
、PIDTYPE_TGID
等)返回对应的pid
结构体。
- 每个任务(进程或线程)在内核中都有一个唯一的
-
增加
pid
的引用计数:get_task_pid
会增加pid
的引用计数,确保在使用期间不会被释放。
-
pid_vnr(pid)
:- 将
pid
结构体转换为用户态可见的整数PID
值。
- 将
以下是 get_task_pid
的实现:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/pid.c
struct pid *get_task_pid(struct task_struct *task, enum pid_type type)
{
struct pid *pid;
rcu_read_lock();
pid = get_pid(rcu_dereference(*task_pid_ptr(task, type))); // 增加引用计数
rcu_read_unlock();
return pid;
}
get_task_pid
为新创建的任务获取其对应的 pid
结构体,并增加引用计数。随后,pid_vnr
将其转换为用户态可见的整数 PID
值,用于返回给调用者。(记住此处的 nr
,这就是新创建的子进程的 pid
,笔者这里如此强调是为了下文解释 fork()
函数是如何实现对用户空间返回两个不同值做铺垫。)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
此处为了处理 vfork
,如果设置了 CLONE_VFORK
,初始化 vfork_done
,用于同步父子进程。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
wake_up_new_task(p);
7.4.3. 唤醒新进程加入进程调度队列,以及收尾工作
调用 wake_up_new_task
将新进程加入调度队列,使其可以被调度运行。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
if (unlikely(trace))
ptrace_event_pid(trace, pid);
如果设置了 ptrace
事件,通知调试器。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
如果设置了 CLONE_VFORK
,父进程等待子进程完成。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
put_pid(pid);
return nr;
清理和返回,释放 PID
的引用计数。返回新进程的 PID
。到此一个新进程也就创建完成。
7.5. kernel_clone() 的核心函数 copy_process()
在 kernel_clone()
函数中一个核心的函数就是 copy_process()
,该函数它负责根据传入的参数创建一个新的 task_struct
,并初始化新进程的各种资源(如内存、文件描述符、信号处理等)。该函数签名如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
__latent_entropy struct task_struct *copy_process(
struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
int pidfd = -1, retval;
struct task_struct *p;
struct multiprocess_signals delayed;
struct file *pidfile = NULL;
const u64 clone_flags = args->flags;
struct nsproxy *nsp = current->nsproxy;
/*
* Don't allow sharing the root directory with processes in a different
* namespace
*/
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);
/*
* Thread groups must share signals as well, and detached threads
* can only be started up within the thread group.
*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* Shared signal handlers imply shared VM. By way of the above,
* thread groups also imply shared VM. Blocking this case allows
* for various simplifications in other code.
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/*
* Siblings of global init remain as zombies on exit since they are
* not reaped by their parent (swapper). To solve this and to avoid
* multi-rooted process trees, prevent global and container-inits
* from creating siblings.
*/
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
/*
* If the new process will be in a different pid or user namespace
* do not allow it to share a thread group with the forking task.
*/
if (clone_flags & CLONE_THREAD) {
if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
(task_active_pid_ns(current) != nsp->pid_ns_for_children))
return ERR_PTR(-EINVAL);
}
if (clone_flags & CLONE_PIDFD) {
/*
* - CLONE_DETACHED is blocked so that we can potentially
* reuse it later for CLONE_PIDFD.
*/
if (clone_flags & CLONE_DETACHED)
return ERR_PTR(-EINVAL);
}
/*
* Force any signals received before this point to be delivered
* before the fork happens. Collect up signals sent to multiple
* processes that happen during the fork and delay them so that
* they appear to happen after the fork.
*/
sigemptyset(&delayed.signal);
INIT_HLIST_NODE(&delayed.node);
spin_lock_irq(¤t->sighand->siglock);
if (!(clone_flags & CLONE_THREAD))
hlist_add_head(&delayed.node, ¤t->signal->multiprocess);
recalc_sigpending();
spin_unlock_irq(¤t->sighand->siglock);
retval = -ERESTARTNOINTR;
if (task_sigpending(current))
goto fork_out;
retval = -ENOMEM;
p = dup_task_struct(current, node);
if (!p)
goto fork_out;
p->flags &= ~PF_KTHREAD;
if (args->kthread)
p->flags |= PF_KTHREAD;
if (args->user_worker) {
/*
* Mark us a user worker, and block any signal that isn't
* fatal or STOP
*/
p->flags |= PF_USER_WORKER;
siginitsetinv(&p->blocked, sigmask(SIGKILL)|sigmask(SIGSTOP));
}
if (args->io_thread)
p->flags |= PF_IO_WORKER;
if (args->name)
strscpy_pad(p->comm, args->name, sizeof(p->comm));
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? args->child_tid : NULL;
/*
* Clear TID on mm_release()?
*/
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? args->child_tid : NULL;
ftrace_graph_init_task(p);
rt_mutex_init_task(p);
lockdep_assert_irqs_enabled();
#ifdef CONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
retval = -EAGAIN;
if (is_rlimit_overlimit(task_ucounts(p), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_cleanup_count;
}
current->flags &= ~PF_NPROC_EXCEEDED;
/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
retval = -EAGAIN;
if (data_race(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 | PF_IDLE | PF_NO_SETAFFINITY);
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);
init_sigpending(&p->pending);
p->utime = p->stime = p->gtime = 0;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIME
p->utimescaled = p->stimescaled = 0;
#endif
prev_cputime_init(&p->prev_cputime);
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
seqcount_init(&p->vtime.seqcount);
p->vtime.starttime = 0;
p->vtime.state = VTIME_INACTIVE;
#endif
#ifdef CONFIG_IO_URING
p->io_uring = NULL;
#endif
p->default_timer_slack_ns = current->timer_slack_ns;
#ifdef CONFIG_PSI
p->psi_flags = 0;
#endif
task_io_accounting_init(&p->ioac);
acct_clear_integrals(p);
posix_cputimers_init(&p->posix_cputimers);
tick_dep_init_task(p);
p->io_context = NULL;
audit_set_context(p, NULL);
cgroup_fork(p);
if (args->kthread) {
if (!set_kthread_struct(p))
goto bad_fork_cleanup_delayacct;
}
#ifdef CONFIG_NUMA
p->mempolicy = mpol_dup(p->mempolicy);
if (IS_ERR(p->mempolicy)) {
retval = PTR_ERR(p->mempolicy);
p->mempolicy = NULL;
goto bad_fork_cleanup_delayacct;
}
#endif
#ifdef CONFIG_CPUSETS
p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
seqcount_spinlock_init(&p->mems_allowed_seq, &p->alloc_lock);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
memset(&p->irqtrace, 0, sizeof(p->irqtrace));
p->irqtrace.hardirq_disable_ip = _THIS_IP_;
p->irqtrace.softirq_enable_ip = _THIS_IP_;
p->softirqs_enabled = 1;
p->softirq_context = 0;
#endif
p->pagefault_disabled = 0;
#ifdef CONFIG_LOCKDEP
lockdep_init_task(p);
#endif
#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
p->sequential_io = 0;
p->sequential_io_avg = 0;
#endif
#ifdef CONFIG_BPF_SYSCALL
RCU_INIT_POINTER(p->bpf_storage, NULL);
p->bpf_ctx = NULL;
#endif
/* Perform scheduler related setup. Assign this task to a CPU. */
retval = sched_fork(clone_flags, p);
if (retval)
goto bad_fork_cleanup_policy;
retval = perf_event_init_task(p, clone_flags);
if (retval)
goto bad_fork_sched_cancel_fork;
retval = audit_alloc(p);
if (retval)
goto bad_fork_cleanup_perf;
/* copy all the process information */
shm_init_task(p);
retval = security_task_alloc(p, clone_flags);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_security;
retval = copy_files(clone_flags, p, args->no_files);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread(p, args);
if (retval)
goto bad_fork_cleanup_io;
stackleak_task_init(p);
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
args->set_tid_size);
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
}
}
/*
* This has to happen after we've potentially unshared the file
* descriptor table (so that the pidfd doesn't leak into the child
* if the fd table isn't shared).
*/
if (clone_flags & CLONE_PIDFD) {
int flags = (clone_flags & CLONE_THREAD) ? PIDFD_THREAD : 0;
/*
* Note that no task has been attached to @pid yet indicate
* that via CLONE_PIDFD.
*/
retval = __pidfd_prepare(pid, flags | PIDFD_CLONE, &pidfile);
if (retval < 0)
goto bad_fork_free_pid;
pidfd = retval;
retval = put_user(pidfd, args->pidfd);
if (retval)
goto bad_fork_put_pidfd;
}
#ifdef CONFIG_BLOCK
p->plug = NULL;
#endif
futex_init_task(p);
/*
* sigaltstack should be cleared when sharing the same VM
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
sas_ss_reset(p);
/*
* Syscall tracing and stepping should be turned off in the
* child regardless of CLONE_PTRACE.
*/
user_disable_single_step(p);
clear_task_syscall_work(p, SYSCALL_TRACE);
#if defined(CONFIG_GENERIC_ENTRY) || defined(TIF_SYSCALL_EMU)
clear_task_syscall_work(p, SYSCALL_EMU);
#endif
clear_tsk_latency_tracing(p);
/* ok, now we should be set up.. */
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
p->group_leader = p;
p->tgid = p->pid;
}
p->nr_dirtied = 0;
p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
p->dirty_paused_when = 0;
p->pdeath_signal = 0;
p->task_works = NULL;
clear_posix_cputimers_work(p);
#ifdef CONFIG_KRETPROBES
p->kretprobe_instances.first = NULL;
#endif
#ifdef CONFIG_RETHOOK
p->rethooks.first = NULL;
#endif
/*
* Ensure that the cgroup subsystem policies allow the new process to be
* forked. It should be noted that the new process's css_set can be changed
* between here and cgroup_post_fork() if an organisation operation is in
* progress.
*/
retval = cgroup_can_fork(p, args);
if (retval)
goto bad_fork_put_pidfd;
/*
* Now that the cgroups are pinned, re-clone the parent cgroup and put
* the new task on the correct runqueue. All this *before* the task
* becomes visible.
*
* This isn't part of ->can_fork() because while the re-cloning is
* cgroup specific, it unconditionally needs to place the task on a
* runqueue.
*/
retval = sched_cgroup_fork(p, args);
if (retval)
goto bad_fork_cancel_cgroup;
/*
* From this point on we must avoid any synchronous user-space
* communication until we take the tasklist-lock. In particular, we do
* not want user-space to be able to predict the process start-time by
* stalling fork(2) after we recorded the start_time but before it is
* visible to the system.
*/
p->start_time = ktime_get_ns();
p->start_boottime = ktime_get_boottime_ns();
/*
* Make it visible to the rest of the system, but dont wake it up yet.
* Need tasklist lock for parent etc handling!
*/
write_lock_irq(&tasklist_lock);
/* CLONE_PARENT re-uses the old parent */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
if (clone_flags & CLONE_THREAD)
p->exit_signal = -1;
else
p->exit_signal = current->group_leader->exit_signal;
} else {
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
p->exit_signal = args->exit_signal;
}
klp_copy_process(p);
sched_core_fork(p);
spin_lock(¤t->sighand->siglock);
rv_task_fork(p);
rseq_fork(p, clone_flags);
/* Don't start children in a dying pid namespace */
if (unlikely(!(ns_of_pid(pid)->pid_allocated & PIDNS_ADDING))) {
retval = -ENOMEM;
goto bad_fork_core_free;
}
/* Let kill terminate clone/fork in the middle */
if (fatal_signal_pending(current)) {
retval = -EINTR;
goto bad_fork_core_free;
}
/* No more failure paths after this point. */
/*
* Copy seccomp details explicitly here, in case they were changed
* before holding sighand lock.
*/
copy_seccomp(p);
init_task_pid_links(p);
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
init_task_pid(p, PIDTYPE_TGID, pid);
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
init_task_pid(p, PIDTYPE_SID, task_session(current));
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
p->signal->shared_pending.signal = delayed.signal;
p->signal->tty = tty_kref_get(current->signal->tty);
/*
* Inherit has_child_subreaper flag under the same
* tasklist_lock with adding child to the process tree
* for propagate_has_child_subreaper optimization.
*/
p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||
p->real_parent->signal->is_child_subreaper;
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
attach_pid(p, PIDTYPE_TGID);
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
current->signal->nr_threads++;
current->signal->quick_threads++;
atomic_inc(¤t->signal->live);
refcount_inc(¤t->signal->sigcnt);
task_join_group_stop(p);
list_add_tail_rcu(&p->thread_node,
&p->signal->thread_head);
}
attach_pid(p, PIDTYPE_PID);
nr_threads++;
}
total_forks++;
hlist_del_init(&delayed.node);
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);
if (pidfile)
fd_install(pidfd, pidfile);
proc_fork_connector(p);
sched_post_fork(p);
cgroup_post_fork(p, args);
perf_event_fork(p);
trace_task_newtask(p, clone_flags);
uprobe_copy_process(p, clone_flags);
user_events_fork(p, clone_flags);
copy_oom_score_adj(clone_flags, p);
return p;
bad_fork_core_free:
sched_core_free(p);
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
bad_fork_cancel_cgroup:
cgroup_cancel_fork(p, args);
bad_fork_put_pidfd:
if (clone_flags & CLONE_PIDFD) {
fput(pidfile);
put_unused_fd(pidfd);
}
bad_fork_free_pid:
if (pid != &init_struct_pid)
free_pid(pid);
bad_fork_cleanup_thread:
exit_thread(p);
bad_fork_cleanup_io:
if (p->io_context)
exit_io_context(p);
bad_fork_cleanup_namespaces:
exit_task_namespaces(p);
bad_fork_cleanup_mm:
if (p->mm) {
mm_clear_owner(p->mm, p);
mmput(p->mm);
}
bad_fork_cleanup_signal:
if (!(clone_flags & CLONE_THREAD))
free_signal_struct(p->signal);
bad_fork_cleanup_sighand:
__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
exit_fs(p); /* blocking */
bad_fork_cleanup_files:
exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
exit_sem(p);
bad_fork_cleanup_security:
security_task_free(p);
bad_fork_cleanup_audit:
audit_free(p);
bad_fork_cleanup_perf:
perf_event_free_task(p);
bad_fork_sched_cancel_fork:
sched_cancel_fork(p);
bad_fork_cleanup_policy:
lockdep_free_task(p);
#ifdef CONFIG_NUMA
mpol_put(p->mempolicy);
#endif
bad_fork_cleanup_delayacct:
delayacct_tsk_free(p);
bad_fork_cleanup_count:
dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
exit_creds(p);
bad_fork_free:
WRITE_ONCE(p->__state, TASK_DEAD);
exit_task_stack_account(p);
put_task_stack(p);
delayed_free_task(p);
fork_out:
spin_lock_irq(¤t->sighand->siglock);
hlist_del_init(&delayed.node);
spin_unlock_irq(¤t->sighand->siglock);
return ERR_PTR(retval);
}
-
参数:
pid
:新进程的PID
(如果为NULL
,将在函数中分配)。trace
:是否需要跟踪(ptrace
)新进程。node
:NUMA
节点,用于分配内存。args
:包含创建新进程所需的参数(如flags
、stack
、tls
等)。
-
返回值:
- 成功时返回新创建的
task_struct
。 - 失败时返回错误指针(
ERR_PTR
)。
- 成功时返回新创建的
7.5.1. 参数检查阶段
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
这段内容对 clone_flags
所传递的标志组合进行合法性检查。
- CLONE_NEWNS 和 CLONE_FS: 检查是否允许共享根目录 ,如果设置了
CLONE_NEWNS
,则不能同时设置CLONE_FS
,因为CLONE_NEWNS
会创建一个新的挂载命名空间,而CLONE_FS
要求共享文件系统上下文,这两者是冲突的。; - CLONE_NEWUSER 和 CLONE_FS: 检查是否允许共享用户命名空间,同上,两者不可同时被设置;
- CLONE_THREAD 和 CLONE_SIGHAND: 检查线程组的信号和内存共享标志是否一致。如果设置了
CLONE_THREAD
标志(表示创建线程),但没有设置CLONE_SIGHAND
标志(表示共享信号处理表),则返回错误.线程组中的所有线程必须共享信号处理表(sighand_struct
),这是线程组的基本要求。
如果不共享信号处理表,线程之间无法正确处理信号,这会导致线程组行为异常; - CLONE_SIGHAND 和 CLONE_VM: 共享信号处理表的线程组必须共享内存地址空间(
mm_struct
),这是线程组的另一个基本要求。
如果线程组不共享内存地址空间,线程之间无法正确共享信号队列和其他资源,这会导致线程组行为不一致。设置CLONE_SIGHAND
必须同时设置CLONE_VM
,否则返回-EINVAL
。
通过这几个参数你应该发现了什么,没错,该函数不仅是用来创建进程的,同时也是拿来创建线程的,对于 Linux
内核而言事实上对进程与线程是一视同仁的(同类东西),通过后文的继续学习将会了解到这一点。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
if ((clone_flags & CLONE_PARENT) &&
current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
if (clone_flags & CLONE_THREAD) {
if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
(task_active_pid_ns(current) != nsp->pid_ns_for_children))
return ERR_PTR(-EINVAL);
}
if (clone_flags & CLONE_PIDFD) {
/*
* - CLONE_DETACHED is blocked so that we can potentially
* reuse it later for CLONE_PIDFD.
*/
if (clone_flags & CLONE_DETACHED)
return ERR_PTR(-EINVAL);
}
- CLONE_PARENT 和 SIGNAL_UNKILLABLE:设置
CLONE_PARENT
标志表示新进程与当前进程共享父进程,SIGNAL_UNKILLABLE
表示进程不能被杀死,通常用于标记全局init
进程或容器的init
进程。如果设置了CLONE_PARENT
标志,并且当前进程的信号标志中包含SIGNAL_UNKILLABLE
,则返回错误;(如果当前进程是全局或容器的init
进程,则禁止使用CLONE_PARENT
创建兄弟进程。全局或容器的init
进程不能创建兄弟进程,因为这些兄弟进程在退出时会变成僵尸进程,无法被init
进程回收,导致多根进程树的问题) - CLONE_THREAD 和 CLONE_NEWUSER、CLONE_NEWPID: 设置
CLONE_THREAD
时,不能同时设置CLONE_NEWUSER
或CLONE_NEWPID
,且必须确保线程组的命名空间一致。这是因为线程组中的所有线程必须共享相同的用户和PID
命名空间。
如果创建线程时尝试切换到新的用户或PID
命名空间,会破坏线程组的基本约束; - CLONE_DETACHED 和 CLONE_PIDFD:设置
CLONE_PIDFD
时,不能同时设置CLONE_DETACHED
。CLONE_PIDFD
表示为新进程创建一个PID
文件描述符,CLONE_DETACHED
是一个废弃的标志,曾用于创建与父进程分离的子进程。为了避免冲突并保留CLONE_DETACHED
的潜在用途,内核禁止它与CLONE_PIDFD
同时使用。
总的来说就是,全局或容器的 init
进程不能创建兄弟进程(CLONE_PARENT
)。
创建线程(CLONE_THREAD
)时,不能切换用户或 PID
命名空间。
创建 PID
文件描述符(CLONE_PIDFD
)时,不能使用废弃的 CLONE_DETACHED
标志。
7.5.2. 信号处理
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
// 初始化 delayed.signal,将其设置为空信号集。保存拷贝进程操作期间需要延迟处理的信号。
sigemptyset(&delayed.signal);
// 初始化 delayed.node,这是一个哈希链表节点,用于将延迟的信号添加到当前进程的信号链表中。
INIT_HLIST_NODE(&delayed.node);
spin_lock_irq(¤t->sighand->siglock);
if (!(clone_flags & CLONE_THREAD))
hlist_add_head(&delayed.node, ¤t->signal->multiprocess);
recalc_sigpending();
spin_unlock_irq(¤t->sighand->siglock);
retval = -ERESTARTNOINTR;
if (task_sigpending(current))
goto fork_out;
-
sigemptyset
:- 初始化
delayed.signal
,将其设置为空信号集; - 这是为了存储在拷贝进程操作期间需要延迟处理的信号。
- 初始化
-
INIT_HLIST_NODE
:- 初始化
delayed.node
,这是一个哈希链表节点,用于将延迟的信号添加到当前进程的信号链表中。
- 初始化
-
spin_lock_irq
:- 获取当前进程的信号锁(
siglock
),以保护信号相关的数据结构免受并发修改。
- 获取当前进程的信号锁(
-
if (!(clone_flags & CLONE_THREAD))
:- 检查是否设置了
CLONE_THREAD
标志:- 如果未设置,表示这是一个新进程,而不是线程。
- 将
delayed.node
添加到当前进程的multiprocess
信号链表中,用于延迟处理多进程信号。
- 检查是否设置了
-
hlist_add_head
:- 将
delayed.node
添加到current->signal->multiprocess
链表的头部。
- 将
-
recalc_sigpending
:- 重新计算当前进程的挂起信号状态。
- 如果有挂起的信号需要处理,会更新相关标志。
-
spin_unlock_irq
:- 释放信号锁,允许其他线程或进程访问信号相关的数据结构。
-
task_sigpending
:- 检查当前进程是否有挂起的信号需要处理。
- 如果有挂起信号,返回非零值。
-
goto fork_out
:- 如果有挂起信号,跳转到
fork_out
标签,终止当前拷贝操作,并返回错误代码-ERESTARTNOINTR
。 - 这通常表示系统调用需要被重新启动。
- 如果有挂起信号,跳转到
7.5.3. 分配并初始化 task_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = -ENOMEM;
p = dup_task_struct(current, node);
if (!p)
goto fork_out;
-
错误码
-ENOMEM
:- 表示内存不足(
Out of Memory
),这是内核中常见的错误码,用于指示内存分配失败。如果后续的dup_task_struct
调用失败,函数会跳转到fork_out
标签,并返回这个错误值。
- 表示内存不足(
-
调用
dup_task_struct
:- 复制当前进程(
current
)的task_struct
,并为新进程分配一个新的task_struct
。 task_struct
是内核中描述进程的核心数据结构(进程描述符),包含进程的状态、调度信息、内存管理信息等。-
参数说明:
current
:当前进程的task_struct
,作为复制的模板。node
:NUMA
节点,用于指定在哪个内存节点上分配新进程的task_struct
。
-
返回值:
- 如果成功,返回新分配的
task_struct
的指针。 - 如果失败,返回
NULL
。
- 如果成功,返回新分配的
-
- 复制当前进程(
7.5.4. 初始化新进程的标志位 task_struct->flag
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
// 清除 PF_KTHREAD 标志,假设新进程是普通用户进程。
p->flags &= ~PF_KTHREAD;
// 如果 args->kthread 为真
if (args->kthread)
// 设置 PF_KTHREAD 标志,表明新进程是一个内核线程。
p->flags |= PF_KTHREAD;
// 判断传入参数是否设置一个用户工作线程
if (args->user_worker) {
/*
* Mark us a user worker, and block any signal that isn't
* fatal or STOP
*/
// 设置新task->flag标记为一个用户工作线程(User Worker Thread)。
p->flags |= PF_USER_WORKER;
// 屏蔽所有信号,除了 SIGKILL 和 SIGSTOP
siginitsetinv(&p->blocked, sigmask(SIGKILL)|sigmask(SIGSTOP));
}
if (args->io_thread)
// 标记新进程falg是一个 IO 工作线程(IO Worker Thread)
p->flags |= PF_IO_WORKER;
if (args->name)
// 根据参数设置新进程的名称,通常用于调试和监控。
strscpy_pad(p->comm, args->name, sizeof(p->comm));
/*
* 如果 args->name 不为空,则将其复制到 p->comm 中。
* 使用 strscpy_pad 确保字符串被安全地复制,并填充到固定长度。
* 进程名称的作用,例如,在 ps 命令中可以看到进程的名称。
*/
// 如果设置了 CLONE_CHILD_SETTID,则初始化 p->set_child_tid。
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? args->child_tid : NULL;
/*
* Clear TID on mm_release()?
*/
// 如果设置了 CLONE_CHILD_CLEARTID,则初始化 p->clear_child_tid。
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? args->child_tid : NULL;
[注]:笔者将代码功能写入代码注释中,说明这里并非我们理解函数功能的主要代码段,根据注释简单了解此处的作用即可。下文此类操作同理。
这段代码主要是根据传入的参数设置新进程的标志位初始化进程的标记字段。并且设置线程 ID
相关字段(set_child_tid
和 clear_child_tid
)。这些设置确保新进程的行为符合调用者的要求,并为后续的调度和信号处理做好准备。
7.5.5. 为复制父进程状态做相关准备工作
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
// 初始化新进程的函数调用跟踪(Function Graph Tracing)相关数据。
ftrace_graph_init_task(p);
/*
* ftrace 是 Linux 内核中的一个跟踪框架,用于记录函数调用的执行路径和性能数据。
* 该函数为新进程的 task_struct 设置跟踪相关的初始状态。
* 如果启用了 ftrace 功能,内核可以跟踪函数调用的执行路径,帮助我们调试和分析性能。
* 每个进程都需要独立的跟踪状态,因此在创建新进程时需要初始化。
*/
// 初始化新进程的实时互斥锁(RT Mutex)相关数据。
rt_mutex_init_task(p);
/*
* 主要用于支持实时调度(Real-Time Scheduling)和优先级继承(Priority Inheritance)。
* 实时互斥锁是 Linux 内核中用于解决优先级反转问题的一种机制。
* 在创建新进程时,需要初始化这些字段以支持实时调度。
* 函数大致工作流程:
* 初始化新进程的 pi_lock(优先级继承锁)。
* 如果启用了 CONFIG_RT_MUTEXES 配置,还会初始化其他与实时互斥锁相关的字段,例如:
* pi_waiters:等待该任务的实时互斥锁队列;
* pi_top_task:当前阻塞的最高优先级任务;
* pi_blocked_on:当前任务被哪个锁阻塞。
*/
// 确保在执行到此处时,中断(IRQs)是启用状态,如果中断被禁用,会触发警告或错误。
// 内核中的许多操作需要在中断启用的情况下执行,以确保系统的正常运行。
lockdep_assert_irqs_enabled();
#ifdef CONFIG_PROVE_LOCKING
// 如果启用了 CONFIG_PROVE_LOCKING 配置,则检查新进程的 softirqs_enabled 状态。如果软中断(Soft IRQs)未启用,会触发警告。
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
/*
* is_rlimit_overlimit:
* 检查当前用户是否超过了 RLIMIT_NPROC(最大进程数限制)。
* 如果超过限制且用户不是 INIT_USER,并且没有 CAP_SYS_RESOURCE 或 CAP_SYS_ADMIN 权限,则返回错误。
* PF_NPROC_EXCEEDED:
* 如果进程数限制被触发,当前进程的标志位 PF_NPROC_EXCEEDED 会被设置。
* 在这里清除该标志位,表示限制已被处理。
*/
retval = -EAGAIN;
if (is_rlimit_overlimit(task_ucounts(p), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_cleanup_count;
}
current->flags &= ~PF_NPROC_EXCEEDED;
/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
/*
* nr_threads:
* 当前系统中的线程总数。如果线程数超过了 max_threads(系统允许的最大线程数),返回错误。
* 这是为了防止线程爆炸(例如 fork 炸弹)导致系统资源耗尽。
*/
retval = -EAGAIN;
if (data_race(nr_threads >= max_threads))
goto bad_fork_cleanup_count;
// 初始化延迟统计(Delay Accounting)相关数据,用于记录进程的调度延迟、IO 延迟等。
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
/*
* 清除标志位:
* PF_SUPERPRIV:表示进程具有超级用户权限。
* PF_WQ_WORKER:表示进程是工作队列的工作线程。
* PF_IDLE:表示进程是空闲线程。
* PF_NO_SETAFFINITY:表示进程不能设置 CPU 亲和性。
*/
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER | PF_IDLE | PF_NO_SETAFFINITY);
// 设置标志位 PF_FORKNOEXEC:表示新进程尚未执行任何程序(exec)。
p->flags |= PF_FORKNOEXEC;
// 初始化子进程链表,用于记录当前进程的子进程。
INIT_LIST_HEAD(&p->children);
// 初始化兄弟进程链表,用于记录当前进程在父进程的子进程链表中的位置。
INIT_LIST_HEAD(&p->sibling);
// 初始化与 RCU(Read-Copy-Update)相关的数据结构。RCU 是一种高效的读写同步机制,用于内核中的并发操作。
rcu_copy_process(p);
// 用于 vfork 系统调用的同步机制,初始化为 NULL。
p->vfork_done = NULL;
// 初始化分配锁,用于保护进程的内存分配操作。
spin_lock_init(&p->alloc_lock);
// 初始化挂起信号队列,用于存储未处理的信号。
init_sigpending(&p->pending);
// 下面用于初始化 CPU 时间统计
// 初始化用户时间、系统时间和全局时间统计。(utime(user time)、stime(system time)、gtime(global time))
p->utime = p->stime = p->gtime = 0;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIME
p->utimescaled = p->stimescaled = 0;
#endif
// 初始化前一次 CPU 时间统计。
prev_cputime_init(&p->prev_cputime);
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
seqcount_init(&p->vtime.seqcount);
p->vtime.starttime = 0;
p->vtime.state = VTIME_INACTIVE;
#endif
#ifdef CONFIG_IO_URING
p->io_uring = NULL;
#endif
// 继承父进程的定时器松弛值。(current 获取当前进程 task_struct 的宏,即新进程的父进程)
p->default_timer_slack_ns = current->timer_slack_ns;
#ifdef CONFIG_PSI
p->psi_flags = 0;
#endif
// 初始化 IO 统计数据。
task_io_accounting_init(&p->ioac);
// 清除进程的会计统计数据。
acct_clear_integrals(p);
// 初始化 POSIX CPU 定时器。
posix_cputimers_init(&p->posix_cputimers);
// 初始化与时钟滴答相关的数据。
tick_dep_init_task(p);
p->io_context = NULL;
audit_set_context(p, NULL);
cgroup_fork(p);
if (args->kthread) {
if (!set_kthread_struct(p))
goto bad_fork_cleanup_delayacct;
}
/*
* 如果启用了 NUMA(非一致性内存访问),复制内存策略(mempolicy)。
* 如果复制失败,返回错误并清理。
*/
#ifdef CONFIG_NUMA
p->mempolicy = mpol_dup(p->mempolicy);
if (IS_ERR(p->mempolicy)) {
retval = PTR_ERR(p->mempolicy);
p->mempolicy = NULL;
goto bad_fork_cleanup_delayacct;
}
#endif
// 初始化 CPU 集相关的数据结构。
#ifdef CONFIG_CPUSETS
p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
seqcount_spinlock_init(&p->mems_allowed_seq, &p->alloc_lock);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
memset(&p->irqtrace, 0, sizeof(p->irqtrace));
// 初始化硬中断和软中断的跟踪信息。
p->irqtrace.hardirq_disable_ip = _THIS_IP_;
p->irqtrace.softirq_enable_ip = _THIS_IP_;
p->softirqs_enabled = 1;
p->softirq_context = 0;
#endif
p->pagefault_disabled = 0;
// 初始化锁依赖性检查相关数据。
#ifdef CONFIG_LOCKDEP
lockdep_init_task(p);
#endif
// 初始化互斥锁调试信息。
#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
p->sequential_io = 0;
p->sequential_io_avg = 0;
#endif
#ifdef CONFIG_BPF_SYSCALL
RCU_INIT_POINTER(p->bpf_storage, NULL);
p->bpf_ctx = NULL;
#endif
-
copy_creds
:- 复制当前进程的权限信息(
cred
),包括用户ID
、组ID
、能力(capabilities
)等。 - 如果复制失败,返回错误码并跳转到
bad_fork_free
标签进行清理。
每个进程都有独立的权限信息,copy_creds
确保新进程的权限信息与父进程一致,或者根据clone_flags
进行调整。
- 复制当前进程的权限信息(
这段代码的主要功能是检查权限和资源限制,确保新进程的创建符合系统约束。初始化新进程的各种数据结构,包括权限、锁、信号、时间统计等。设置新进程的标志位和链表结构,为后续的调度和管理做好准备。通过这些步骤,内核确保新进程的状态是干净且一致的,为后续的运行提供了基础。
7.5.6. 初始化新进程调度相关信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
/* Perform scheduler related setup. Assign this task to a CPU. */
/*
* 调用 sched_fork 函数完成新进程的调度相关初始化。
* 参数:
* clone_flags:fork 或 clone 系统调用传递的标志,用于控制新进程的行为。
* p:新创建的进程的 task_struct。
* 主要工作:
* 将新进程分配到一个 CPU。
* 初始化调度器相关的数据结构(如优先级、时间片等)。
* 设置新进程的调度策略。
* 调度器是内核的核心组件之一,负责管理进程的运行。
* 在创建新进程时,必须为其设置调度相关的初始状态。
*/
retval = sched_fork(clone_flags, p);
if (retval)
// 清理调度相关的资源。
goto bad_fork_cleanup_policy;
/*
* 调用 perf_event_init_task 函数初始化新进程的性能事件(Performance Events)。
* 为新进程分配性能监控相关的数据结构。
* 继承父进程的性能事件上下文(如果需要)。
* perf_event 是 Linux 内核中的性能监控框架,用于跟踪和分析系统性能。
*/
retval = perf_event_init_task(p, clone_flags);
if (retval)
// 清理性能事件相关的资源。
goto bad_fork_sched_cancel_fork;
/*
* 调用 audit_alloc 函数为新进程分配审计(Audit)相关的数据结构。
* 初始化审计上下文。继承父进程的审计信息(如果需要)。
* 审计子系统用于记录系统调用和其他安全相关事件。
* 在创建新进程时,需要为其分配审计上下文,以便记录其活动。
*/
retval = audit_alloc(p);
if (retval)
// 清理审计相关的资源。
goto bad_fork_cleanup_perf;
这段代码的主要确保新进程在调度、性能监控和审计方面的状态是正确的。如果任何步骤失败,代码会进行相应的清理,避免资源泄漏或状态不一致。
7.5.7. 复制父进程内容并分配 PID
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
/* copy all the process information */
shm_init_task(p);
retval = security_task_alloc(p, clone_flags);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_security;
retval = copy_files(clone_flags, p, args->no_files);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread(p, args);
if (retval)
goto bad_fork_cleanup_io;
stackleak_task_init(p);
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
args->set_tid_size);
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
}
}
这段代码是 copy_process
函数中对新进程的资源和上下文进行初始化和复制的部分,涉及共享内存、权限、安全性、文件系统、信号处理、内存管理、命名空间、IO 上下文、线程上下文等多个方面。
1. 共享内存初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
shm_init_task(p);
-
作用:
- 初始化新进程的共享内存(
System V
共享内存)相关数据结构。 - 如果父进程有共享内存段,新进程会继承这些信息。
- 初始化新进程的共享内存(
-
背景:
- 共享内存是进程间通信的一种方式,
shm_init_task
确保新进程能够正确处理共享内存。
- 共享内存是进程间通信的一种方式,
2. 安全性相关初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = security_task_alloc(p, clone_flags);
if (retval)
goto bad_fork_cleanup_audit;
-
作用:
- 调用安全模块(如
SELinux
、AppArmor
)为新进程分配安全上下文。 - 根据
clone_flags
设置新进程的安全属性。
- 调用安全模块(如
-
返回值:
- 如果分配失败,返回错误码并跳转到
bad_fork_cleanup_audit
标签进行清理。
- 如果分配失败,返回错误码并跳转到
-
背景:
Linux
内核中的安全模块需要为每个进程维护独立的安全上下文,以实现访问控制。
3. 信号量撤销信息复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_security;
-
作用:
- 复制父进程的信号量撤销(
semaphore undo
)信息到新进程。 - 如果父进程在使用
System V
信号量,新进程需要继承这些信息。
- 复制父进程的信号量撤销(
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_security
标签进行清理。
- 如果复制失败,返回错误码并跳转到
4. 文件描述符表复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_files(clone_flags, p, args->no_files);
if (retval)
goto bad_fork_cleanup_semundo;
-
作用:
- 根据
clone_flags
决定是否共享文件描述符表。 - 如果未共享,调用
dup_fd
函数复制父进程的文件描述符表。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_semundo
标签进行清理。
- 如果复制失败,返回错误码并跳转到
5. 文件系统信息复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
-
作用:
- 根据
clone_flags
决定是否共享文件系统信息(如当前工作目录和根目录)。 - 如果未共享,调用
copy_fs_struct
函数复制父进程的文件系统信息。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_files
标签进行清理。
- 如果复制失败,返回错误码并跳转到
6. 信号处理表复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
-
作用:
- 根据
clone_flags
决定是否共享信号处理表。 - 如果未共享,调用
kmem_cache_alloc
分配新的信号处理表,并复制父进程的信号处理信息。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_fs
标签进行清理。
- 如果复制失败,返回错误码并跳转到
7.信号队列复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
-
作用:
- 如果新进程是线程组的第一个线程,分配并初始化信号队列。
- 复制父进程的信号队列信息。
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_sighand
标签进行清理。
- 如果复制失败,返回错误码并跳转到
8. 内存管理信息复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
-
作用:
- 根据
clone_flags
决定是否共享内存地址空间。 - 如果未共享,调用
dup_mm
函数复制父进程的内存管理信息。此处也是实现fork()
函数写时拷贝(Copy-On-Write, COW
)机制的关键所在。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_signal
标签进行清理。
- 如果复制失败,返回错误码并跳转到
9. 命名空间复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
-
作用:
- 根据
clone_flags
决定是否共享命名空间(如网络命名空间、PID 命名空间等)。 - 如果未共享,调用
create_new_namespaces
函数创建新的命名空间。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_mm
标签进行清理。
- 如果复制失败,返回错误码并跳转到
10. IO 上下文复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
-
作用:
- 根据
clone_flags
决定是否共享IO
上下文。 - 如果未共享,调用
copy_io_context
函数复制父进程的IO
上下文。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_namespaces
标签进行清理。
- 如果复制失败,返回错误码并跳转到
11. 进程上下文复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_thread(p, args);
if (retval)
goto bad_fork_cleanup_io;
-
作用:
- 复制父进程的进程上下文(如寄存器状态、内核栈等)。
- 初始化新进程的内核栈和寄存器。
- 该函数将作为
fork()
函数实现向用户态函数返回两个不同值的关键步骤。
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_io
标签进行清理。
- 如果复制失败,返回错误码并跳转到
12. 堆栈泄漏保护初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
stackleak_task_init(p);
- 作用:
- 初始化堆栈泄漏保护机制,清除新线程的内核栈内容。
- 这是一个安全功能,用于防止内核栈数据泄漏到用户空间。
13. PID 分配
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
args->set_tid_size);
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
}
}
-
作用:
- 为新进程分配一个唯一的
PID
。 - 如果新进程属于一个新的
PID
命名空间,则分配的PID
可能与父命名空间不同。
- 为新进程分配一个唯一的
-
返回值:
- 如果分配失败,返回错误码并跳转到
bad_fork_cleanup_thread
标签进行清理。
- 如果分配失败,返回错误码并跳转到
这段代码的主要为新进程复制父进程各种资源和上下文,包括共享内存、权限、安全性、文件系统、信号处理、内存管理、命名空间、IO
上下文和线程上下文。最后为新进程分配一个唯一的 PID
,完成进程创建的关键步骤。
7.5.8. 进一步初始化新进程
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
/*
* This has to happen after we've potentially unshared the file
* descriptor table (so that the pidfd doesn't leak into the child
* if the fd table isn't shared).
*/
// 如果设置了 CLONE_PIDFD 标志,内核会为新进程创建一个 PID 文件描述符(pidfd)。
if (clone_flags & CLONE_PIDFD) {
int flags = (clone_flags & CLONE_THREAD) ? PIDFD_THREAD : 0;
/*
* Note that no task has been attached to @pid yet indicate
* that via CLONE_PIDFD.
*/
// 为新进程分配一个 PID 文件描述符,并创建与之关联的文件对象。
retval = __pidfd_prepare(pid, flags | PIDFD_CLONE, &pidfile);
if (retval < 0)
goto bad_fork_free_pid;
pidfd = retval;
// 将分配的 pidfd 写入用户空间的 args->pidfd 指针。
retval = put_user(pidfd, args->pidfd);
if (retval)
goto bad_fork_put_pidfd;
}
// 如果启用了块设备支持,初始化新进程的 plug 字段为 NULL。
#ifdef CONFIG_BLOCK
// plug 用于块设备 IO 操作的优化。
p->plug = NULL;
#endif
// 初始化新进程的 Futex(快速用户空间互斥锁)相关数据结构。
// 确保新进程能够正确处理 Futex 操作。
futex_init_task(p);
/*
* sigaltstack should be cleared when sharing the same VM
*/
// 如果新进程与父进程共享内存地址空间(CLONE_VM),但未设置 CLONE_VFORK,则清除新进程的备用信号栈(sigaltstack)。
// 这是为了避免共享内存时信号栈的冲突。
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
sas_ss_reset(p);
/*
* Syscall tracing and stepping should be turned off in the
* child regardless of CLONE_PTRACE.
*/
// 禁用新进程的单步调试功能。
user_disable_single_step(p);
// 清除新进程的系统调用跟踪标志(SYSCALL_TRACE)。
// 如果启用了系统调用仿真(SYSCALL_EMU),也会清除相关标志。
clear_task_syscall_work(p, SYSCALL_TRACE);
#if defined(CONFIG_GENERIC_ENTRY) || defined(TIF_SYSCALL_EMU)
clear_task_syscall_work(p, SYSCALL_EMU);
#endif
// 清除新进程的延迟跟踪数据。
clear_tsk_latency_tracing(p);
/* ok, now we should be set up.. */
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
p->group_leader = p;
p->tgid = p->pid;
}
// 初始化新进程的脏页计数为 0。
p->nr_dirtied = 0;
// 设置脏页计数的暂停阈值。
p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
// 初始化脏页暂停时间戳为 0。
p->dirty_paused_when = 0;
// 初始化新进程的父进程死亡信号为 0。
p->pdeath_signal = 0;
// 初始化新进程的任务工作队列为 NULL。
p->task_works = NULL;
// 清除新进程的 POSIX CPU 定时器工作队列。
clear_posix_cputimers_work(p);
// 如果启用了 Kretprobe(内核返回探针),初始化相关字段为 NULL。
#ifdef CONFIG_KRETPROBES
p->kretprobe_instances.first = NULL;
#endif
// 如果启用了 Rethook(返回钩子),初始化相关字段为 NULL。
#ifdef CONFIG_RETHOOK
p->rethooks.first = NULL;
#endif
1. 设置 PID 和线程组信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
p->group_leader = p;
p->tgid = p->pid;
}
-
p->pid
:- 设置新进程的
PID
。
- 设置新进程的
-
线程组设置:
- 如果设置了
CLONE_THREAD
,新进程属于与父进程相同的线程组:group_leader
指向线程组的组长。tgid
(线程组ID
)与父进程相同。
- 否则,新进程是一个独立的进程:
group_leader
指向自身。tgid
等于其PID
。
- 如果设置了
7.5.9. 初始化 cgroup 子系统相关、时间戳、父子关系等
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
// 调用 cgroup_can_fork 检查 cgroup 子系统是否允许新进程的创建。
// cgroup 是 Linux 内核中的控制组,用于限制、记录和隔离进程的资源使用(如 CPU、内存、IO 等)。
retval = cgroup_can_fork(p, args);
if (retval)
goto bad_fork_put_pidfd;
/*
* Now that the cgroups are pinned, re-clone the parent cgroup and put
* the new task on the correct runqueue. All this *before* the task
* becomes visible.
*
* This isn't part of ->can_fork() because while the re-cloning is
* cgroup specific, it unconditionally needs to place the task on a
* runqueue.
*/
// 调用 sched_cgroup_fork 根据 args 参数初始化新进程的调度器相关的 cgroup 信息。
retval = sched_cgroup_fork(p, args);
if (retval)
goto bad_fork_cancel_cgroup;
/*
* From this point on we must avoid any synchronous user-space
* communication until we take the tasklist-lock. In particular, we do
* not want user-space to be able to predict the process start-time by
* stalling fork(2) after we recorded the start_time but before it is
* visible to the system.
*/
p->start_time = ktime_get_ns();
p->start_boottime = ktime_get_boottime_ns();
/*
* Make it visible to the rest of the system, but dont wake it up yet.
* Need tasklist lock for parent etc handling!
*/
write_lock_irq(&tasklist_lock);
/* CLONE_PARENT re-uses the old parent */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
if (clone_flags & CLONE_THREAD)
p->exit_signal = -1;
else
p->exit_signal = current->group_leader->exit_signal;
} else {
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
p->exit_signal = args->exit_signal;
}
// 调用 klp_copy_process 复制与内核实时补丁(Kernel Live Patching)相关的状态到新进程
klp_copy_process(p);
// 调用 sched_core_fork 初始化新进程的核心调度信息。
sched_core_fork(p);
// 获取当前进程的信号锁(siglock),确保对信号相关数据的修改是线程安全的
spin_lock(¤t->sighand->siglock);
// 如果启用了 RV(Runtime Verification),初始化新进程的 RV 相关状态。
rv_task_fork(p);
// 初始化新进程的重启序列(Restartable Sequences)状态。
rseq_fork(p, clone_flags);
/* Don't start children in a dying pid namespace */
// 检查新进程所在的 PID 命名空间是否处于有效状态。
if (unlikely(!(ns_of_pid(pid)->pid_allocated & PIDNS_ADDING))) {
retval = -ENOMEM;
goto bad_fork_core_free;
}
/* Let kill terminate clone/fork in the middle */
// 检查当前进程是否有挂起的致命信号。
if (fatal_signal_pending(current)) {
retval = -EINTR;
goto bad_fork_core_free;
}
1. 设置进程的启动时间
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
p->start_time = ktime_get_ns();
p->start_boottime = ktime_get_boottime_ns();
-
p->start_time
:- 设置新进程的启动时间(以纳秒为单位),用于记录进程的创建时间。
-
p->start_boottime
:- 设置新进程的启动时间相对于系统启动时间的偏移量。
-
背景:
- 这些时间戳用于统计和监控进程的生命周期。
4. 获取 tasklist_lock
锁并设置父子关系
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
write_lock_irq(&tasklist_lock);
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
if (clone_flags & CLONE_THREAD)
p->exit_signal = -1;
else
p->exit_signal = current->group_leader->exit_signal;
} else {
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
p->exit_signal = args->exit_signal;
}
-
tasklist_lock
:- 获取
tasklist_lock
锁,确保对进程列表的修改是线程安全的。
- 获取
-
父子关系设置:
- 如果设置了
CLONE_PARENT
或CLONE_THREAD
:- 新进程的父进程为当前进程的真实父进程(
real_parent
)。 - 如果是线程(
CLONE_THREAD
),设置exit_signal
为-1
,表示线程退出时不发送信号。 - 否则,继承父进程的
exit_signal
。
- 新进程的父进程为当前进程的真实父进程(
- 如果未设置
CLONE_PARENT
或CLONE_THREAD
:- 新进程的父进程为当前进程。
- 设置
exit_signal
为传入的参数值。
- 如果设置了
7.5.10. 将新进程添加进内核任务列表并返回
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
/*
* Copy seccomp details explicitly here, in case they were changed
* before holding sighand lock.
*/
// 复制当前进程的 Seccomp(安全计算模式)状态到新进程。
// Seccomp 是一种安全机制,用于限制进程可以调用的系统调用。
// Seccomp 状态需要在新进程中继承,以确保安全策略的一致性。
copy_seccomp(p);
// 初始化新进程的 PID 链接节点(pid_links),为后续的 PID 分配和管理做好准备。
init_task_pid_links(p);
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
init_task_pid(p, PIDTYPE_TGID, pid);
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
init_task_pid(p, PIDTYPE_SID, task_session(current));
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
p->signal->shared_pending.signal = delayed.signal;
p->signal->tty = tty_kref_get(current->signal->tty);
/*
* Inherit has_child_subreaper flag under the same
* tasklist_lock with adding child to the process tree
* for propagate_has_child_subreaper optimization.
*/
p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||
p->real_parent->signal->is_child_subreaper;
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
attach_pid(p, PIDTYPE_TGID);
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
current->signal->nr_threads++;
current->signal->quick_threads++;
atomic_inc(¤t->signal->live);
refcount_inc(¤t->signal->sigcnt);
task_join_group_stop(p);
list_add_tail_rcu(&p->thread_node,
&p->signal->thread_head);
}
attach_pid(p, PIDTYPE_PID);
nr_threads++;
}
total_forks++;
hlist_del_init(&delayed.node);
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);
if (pidfile)
fd_install(pidfd, pidfile);
// 通知用户空间新进程的创建事件。
proc_fork_connector(p);
// 调度器的后续处理,为新进程分配运行队列。
sched_post_fork(p);
// cgroup 子系统的后续处理,将新进程添加到正确的 cgroup 中。
cgroup_post_fork(p, args);
// 初始化新进程的性能事件上下文。
perf_event_fork(p);
// 跟踪新任务的创建事件。
trace_task_newtask(p, clone_flags);
// 复制与用户探针(uprobes)相关的状态。
uprobe_copy_process(p, clone_flags);
// 处理与用户事件相关的 fork 操作。
user_events_fork(p, clone_flags);
// 复制父进程的 OOM(Out-Of-Memory)分数调整值到新进程。
// OOM 分数用于决定进程在内存不足时被杀死的优先级。
copy_oom_score_adj(clone_flags, p);
// 返回新创建的进程的 task_struct 指针。
return p;
1. 设置 PID 和线程组信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
// 检查新进程是否成功分配了有效的 PID。
if (likely(p->pid)) {
// 初始化新进程的 ptrace(进程跟踪)状态。
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
// 将新进程的 PID 与其 task_struct 关联。
init_task_pid(p, PIDTYPE_PID, pid);
// 线程组领导者的特殊处理
if (thread_group_leader(p)) {
// 初始化线程组 ID(TGID),通常等于线程组领导者的 PID。
init_task_pid(p, PIDTYPE_TGID, pid);
// 初始化进程组 ID(PGID),继承父进程的进程组。
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
// 初始化会话 ID(SID),继承父进程的会话。
init_task_pid(p, PIDTYPE_SID, task_session(current));
// 检查是否是子进程回收者
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
// 将其标记为不可杀死(SIGNAL_UNKILLABLE)。
// 子进程回收者是负责回收孤儿进程的特殊进程(如 PID 命名空间中的第一个进程)。
p->signal->flags |= SIGNAL_UNKILLABLE;
}
// 继承父进程的挂起信号。
p->signal->shared_pending.signal = delayed.signal;
// 继承父进程的终端(TTY)引用。
p->signal->tty = tty_kref_get(current->signal->tty);
// 如果父进程是子进程回收者或具有子进程回收者标志,新进程也继承这些标志。
p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||
p->real_parent->signal->is_child_subreaper;
// 将新进程添加到父进程的子进程链表中(children)。
list_add_tail(&p->sibling, &p->real_parent->children);
// 将新进程添加到全局任务链表中(init_task.tasks)。
list_add_tail_rcu(&p->tasks, &init_task.tasks);
// 将新进程的 TGID、PGID 和 SID 附加到全局 PID 哈希表中。
attach_pid(p, PIDTYPE_TGID);
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
// 增加当前 CPU 的进程计数,用于统计。
__this_cpu_inc(process_counts);
} else { // 非线程组领导者的处理
// 增加父进程的线程计数(nr_threads 和 quick_threads)。
current->signal->nr_threads++;
current->signal->quick_threads++;
// 增加线程的活动计数(live)和引用计数(sigcnt)。
atomic_inc(¤t->signal->live);
refcount_inc(¤t->signal->sigcnt);
// 将新线程加入线程组的停止状态。
task_join_group_stop(p);
// 将新线程添加到线程组的线程链表中(thread_head)。
list_add_tail_rcu(&p->thread_node, &p->signal->thread_head);
}
//将新进程的 PID 附加到全局 PID 哈希表中。
attach_pid(p, PIDTYPE_PID);
// 增加全局线程计数(nr_threads)。
nr_threads++;
}
-
ptrace_init_task
:- 初始化新进程的 ptrace(进程跟踪)状态。
- 如果设置了
CLONE_PTRACE
或启用了跟踪,则允许父进程跟踪新进程。
-
init_task_pid
:- 将新进程的
PID
与其task_struct
关联。PIDTYPE_PID
表示这是进程的唯一标识符。
- 将新进程的
-
线程组领导者:
- 如果新进程是线程组的领导者:
- 初始化线程组
ID
(TGID
)、进程组ID
(PGID
)和会话ID
(SID
)。 - 如果新进程是子进程回收者(
child_reaper
),将其标记为不可杀死(SIGNAL_UNKILLABLE
)。 shared_pending.signal
:- 继承父进程的挂起信号。
tty
:- 继承父进程的终端(
TTY
)引用。
- 继承父进程的终端(
- 如果父进程是子进程回收者或具有子进程回收者标志,新进程也继承这些标志。
- 将新进程添加到父进程的子进程链表中。
- 将新进程添加到全局任务链表中。
attach_pid
将新进程的TGID
、PGID
和SID
附加到全局PID
哈希表中。__this_cpu_inc
- 增加当前
CPU
的进程计数,用于统计。
- 增加当前
- 初始化线程组
- 非线程组领导者的处理:
- 增加父进程的线程计数(
nr_threads
和quick_threads
)。 - 增加线程的活动计数(
live
)和引用计数(sigcnt
)。 task_join_group_stop
:- 将新线程加入线程组的停止状态。
list_add_tail_rcu
:- 将新线程添加到线程组的线程链表中(
thread_head
)。
- 将新线程添加到线程组的线程链表中(
- 增加父进程的线程计数(
- 如果新进程是线程组的领导者:
-
attach_pid
:- 将新进程的
PID
附加到全局PID
哈希表中。
- 将新进程的
-
nr_threads
:- 增加全局线程计数。
2. 更新全局统计信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
total_forks++;
hlist_del_init(&delayed.node);
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);
-
total_forks
:- 增加全局 fork 计数。
-
hlist_del_init
:- 从挂起信号链表中删除延迟信号节点。
-
syscall_tracepoint_update
:- 更新新进程的系统调用跟踪点。
-
write_unlock_irq
:- 释放
tasklist_lock
锁,对应上面的write_lock_irq()
。
- 释放
3. 安装 PID 文件描述符
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
if (pidfile)
fd_install(pidfd, pidfile);
- 作用:
- 如果设置了
CLONE_PIDFD
,将新进程的PID
文件描述符安装到父进程的文件描述符表中。
- 如果设置了
这里主要初始化新进程的 PID
和线程组信息。更新全局统计信息并释放锁。安装 PID
文件描述符(如果需要)。通知用户空间并完成调度器、cgroup
和性能事件的后续处理。跟踪新任务的创建并处理用户事件。返回新创建的进程。通过这些操作,copy_process
函数也就完成了新进程的所有初始化工作,并将其交给调度器管理。
7.5.11. 错误处理部分
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
bad_fork_core_free:
// 调用 sched_core_free 释放调度器核心相关的资源。
sched_core_free(p);
// 释放当前进程的信号锁(sighand->siglock)。
spin_unlock(¤t->sighand->siglock);
// 释放全局任务列表锁(tasklist_lock)。
write_unlock_irq(&tasklist_lock);
bad_fork_cancel_cgroup:
// 调用 cgroup_cancel_fork 撤销 cgroup 子系统中为新进程分配的资源
cgroup_cancel_fork(p, args);
bad_fork_put_pidfd:
// 如果设置了 CLONE_PIDFD 标志,释放与 PID 文件描述符相关的资源
if (clone_flags & CLONE_PIDFD) {
fput(pidfile);
put_unused_fd(pidfd);
}
bad_fork_free_pid:
// 如果新进程的 PID 已分配,调用 free_pid 释放 PID。
if (pid != &init_struct_pid)
free_pid(pid);
bad_fork_cleanup_thread:
// 调用 exit_thread 清理与线程相关的资源(如内核栈、线程信息等)。
exit_thread(p);
bad_fork_cleanup_io:
// 如果新进程的 IO 上下文已分配,调用 exit_io_context 释放 IO 上下文。
if (p->io_context)
exit_io_context(p);
bad_fork_cleanup_namespaces:
// 调用 exit_task_namespaces 释放新进程的命名空间资源。
exit_task_namespaces(p);
bad_fork_cleanup_mm:
// 如果新进程的内存管理结构(mm)已分配:
if (p->mm) {
mm_clear_owner(p->mm, p);
mmput(p->mm);
}
bad_fork_cleanup_signal:
// 如果新进程不是线程,调用 free_signal_struct 释放信号结构。
if (!(clone_flags & CLONE_THREAD))
free_signal_struct(p->signal);
bad_fork_cleanup_sighand:
// 调用 __cleanup_sighand 释放信号处理结构。
__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
// 调用 exit_fs 释放文件系统相关的资源(如当前工作目录和根目录)。
exit_fs(p); /* blocking */
bad_fork_cleanup_files:
// 调用 exit_files 释放文件描述符表。
exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
// 调用 exit_sem 释放信号量撤销信息。
exit_sem(p);
bad_fork_cleanup_security:
// 调用 security_task_free 释放安全模块分配的资源。
security_task_free(p);
bad_fork_cleanup_audit:
// 调用 audit_free 释放审计相关的资源。
audit_free(p);
bad_fork_cleanup_perf:
// 调用 perf_event_free_task 释放性能事件相关的资源。
perf_event_free_task(p);
bad_fork_sched_cancel_fork:
// 调用 sched_cancel_fork 撤销调度器中为新进程分配的资源。
sched_cancel_fork(p);
bad_fork_cleanup_policy:
// 调用 lockdep_free_task 释放锁依赖性检查相关的资源。
lockdep_free_task(p);
// 如果启用了 NUMA,调用 mpol_put 释放内存策略。
#ifdef CONFIG_NUMA
mpol_put(p->mempolicy);
#endif
bad_fork_cleanup_delayacct:
// 释放延迟统计相关的资源。
delayacct_tsk_free(p);
bad_fork_cleanup_count:
// 调用 dec_rlimit_ucounts 减少用户进程计数。
dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
// 调用 exit_creds 释放新进程的权限信息。
exit_creds(p);
bad_fork_free:
// 将新进程的状态设置为 TASK_DEAD。
WRITE_ONCE(p->__state, TASK_DEAD);
// 调用 exit_task_stack_account 和 put_task_stack 释放内核栈。
exit_task_stack_account(p);
put_task_stack(p);
// 调用 delayed_free_task 延迟释放任务结构。
delayed_free_task(p);
fork_out:
// 从挂起信号链表中删除延迟信号节点。
spin_lock_irq(¤t->sighand->siglock);
hlist_del_init(&delayed.node);
spin_unlock_irq(¤t->sighand->siglock);
// 返回错误指针,表示进程创建失败。
return ERR_PTR(retval);
主要作用:
- 清理资源:在进程创建失败时,按顺序释放已分配的资源,确保系统状态一致。
- 错误处理路径:通过多个错误处理标签,逐步清理不同类型的资源。
- 返回错误:最终返回错误指针,通知调用者进程创建失败。
这部分为错误处理代码,用于在进程创建失败时清理已分配的资源,确保系统状态一致,避免资源泄漏。
7.6. 实现 fork() 写时拷贝的关键所在 copy_process() 中的 copy_mm()
在上文中介绍进程创建时,很是强调 fork()
函数的一大特点,即写时拷贝,由于将具体拷贝页面的任务更变为,当新进程需要修改时才发生实际拷贝,这样能够加快进程的执行速度,也不会浪费额外内存空间(进程无需修改时便不需要拷贝页面)。那么这么有意思的机制是如何被实现的,这里就来一探究竟。
在执行 copy_process()
函数中会调用到 copy_mm()
函数,按照道理,该函数就是为新进程拷贝父进程的内存空间,那么现在就具体来看看其中发生了什么。
7.6.1. copy_mm 函数
首先看 copy_mm
函数实现如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
/*
* 初始化子进程的内存统计字段:
* * min_flt: 次要缺页错误数。
* * maj_flt: 主要缺页错误数
* * nvcsw: 自愿上下文切换次数。
* * nivcsw: 非自愿上下文切换次数。
*/
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;
tsk->last_switch_time = 0;
#endif
/*
* 初始化新进程的 mm 和 active_mm
* * mm:进程的内存描述符
* * active_mm:当前活动的内存描述符。
*/
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
// 检查父进程是否有 mm_struct
// 如果父进程没有 mm_struct(例如内核线程),直接返回 0
oldmm = current->mm;
if (!oldmm)
return 0;
if (clone_flags & CLONE_VM) {
mmget(oldmm);
mm = oldmm;
} else {
mm = dup_mm(tsk, current->mm);
if (!mm)
return -ENOMEM;
}
// 将子进程的 mm 和 active_mm 设置为新创建或共享的 mm_struct。
tsk->mm = mm;
tsk->active_mm = mm;
// 调用调度器相关的函数,处理与内存上下文 ID(CID)相关的初始化。
sched_mm_cid_fork(tsk);
return 0;
}
7.6.1.1. copy_mm 函数的关键代码段
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_mm()
if (clone_flags & CLONE_VM) {
mmget(oldmm);
mm = oldmm;
}
- 如果设置了
CLONE_VM
标志:- 调用
mmget
增加父进程的mm_struct
的引用计数。 - 子进程直接共享父进程的
mm_struct
。
- 调用
CLONE_VM
的作用:- 表示子进程与父进程共享内存地址空间。即子进程无需单独创建内存空间。
- 通常用于线程(如
pthread
),因为线程需要共享全局变量和堆等内存区域。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_mm()
mm = dup_mm(tsk, current->mm);
if (!mm)
return -ENOMEM;
- 如果没有设置
CLONE_VM
标志,调用dup_mm
为子进程创建一个新的mm_struct
。 dup_mm
的作用:- 复制父进程的
mm_struct
和虚拟内存区域(VMA
)。 - 实现写时拷贝(
Copy-On-Write, COW
)机制。
- 复制父进程的
7.6.2. dup_mm 函数
较为简单内容笔者以注释的方式标注在代码中,比较关键和不易理解的代码将详细摘出分析。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static struct mm_struct *dup_mm(struct task_struct *tsk,
struct mm_struct *oldmm)
{
struct mm_struct *mm;
int err;
// 调用 allocate_mm 函数分配一个新的 mm_struct。
mm = allocate_mm();
if (!mm)
goto fail_nomem;
// 使用 memcpy 将父进程的 mm_struct 内容复制到新分配的 mm_struct 中。
memcpy(mm, oldmm, sizeof(*mm));
if (!mm_init(mm, tsk, mm->user_ns))
goto fail_nomem;
uprobe_start_dup_mmap();
err = dup_mmap(mm, oldmm);
if (err)
goto free_pt;
uprobe_end_dup_mmap();
mm->hiwater_rss = get_mm_rss(mm);
mm->hiwater_vm = mm->total_vm;
if (mm->binfmt && !try_module_get(mm->binfmt->module))
goto free_pt;
return mm;
free_pt:
/* don't put binfmt in mmput, we haven't got module yet */
mm->binfmt = NULL;
mm_init_owner(mm, NULL);
mmput(mm);
if (err)
uprobe_end_dup_mmap();
fail_nomem:
return NULL;
}
7.6.2.1. 复制父进程的 mm_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:dup_mm()
memcpy(mm, oldmm, sizeof(*mm));
- 使用
memcpy
将父进程的mm_struct
内容复制到新分配的mm_struct
中。 - 这会复制父进程的内存布局、虚拟内存区域(
VMA
)等元数据。
7.6.2.2. 初始化新的 mm_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:dup_mm()
if (!mm_init(mm, tsk, mm->user_ns))
goto fail_nomem;
- 调用
mm_init
函数初始化新的mm_struct
。 - 主要完成以下任务:
- 初始化
mm_struct
的引用计数。 - 初始化页表锁、内存映射锁等。
- 设置新的
mm_struct
的用户命名空间。
- 初始化
7.6.2.3. 复制虚拟内存区域(VMA)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:dup_mm()
uprobe_start_dup_mmap();
err = dup_mmap(mm, oldmm);
if (err)
goto free_pt;
uprobe_end_dup_mmap();
- 调用
dup_mmap
函数复制父进程的虚拟内存区域(VMA
)。 dup_mmap
是实现写时拷贝(Copy-On-Write, COW
)的关键函数:- 遍历父进程的
VMA
。 - 为子进程创建对应的
VMA
。 - 不直接复制物理页面,而是通过写时拷贝机制延迟实际的页面分配。
- 遍历父进程的
- 如果
dup_mmap
失败,释放已分配的资源并返回错误。
接着更新内存统计信息,最终返回拷贝完成的新 mm_struct
。
7.6.3. dup_mmap 函数
dup_mmap
函数是 dup_mm
函数处理过程中复制父进程虚拟内存的具体业务函数,先来看下该函数的实现。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static __latent_entropy int dup_mmap(struct mm_struct *mm,
struct mm_struct *oldmm)
{
struct vm_area_struct *mpnt, *tmp;
int retval;
unsigned long charge = 0;
LIST_HEAD(uf);
VMA_ITERATOR(vmi, mm, 0);
// 对父进程的 mm_struct 加写锁,确保在复制过程中不会有其他线程修改内存布局。
if (mmap_write_lock_killable(oldmm))
return -EINTR;
// 调用 flush_cache_dup_mm 刷新父进程的缓存。
flush_cache_dup_mm(oldmm);
// 调用 uprobe_dup_mmap 处理用户探针相关的内存映射。
uprobe_dup_mmap(oldmm, mm);
/*
* Not linked in yet - no deadlock potential:
*/
mmap_write_lock_nested(mm, SINGLE_DEPTH_NESTING);
/* No ordering required: file already has been exposed. */
dup_mm_exe_file(mm, oldmm);
mm->total_vm = oldmm->total_vm;
mm->data_vm = oldmm->data_vm;
mm->exec_vm = oldmm->exec_vm;
mm->stack_vm = oldmm->stack_vm;
/* Use __mt_dup() to efficiently build an identical maple tree. */
retval = __mt_dup(&oldmm->mm_mt, &mm->mm_mt, GFP_KERNEL);
if (unlikely(retval))
goto out;
mt_clear_in_rcu(vmi.mas.tree);
for_each_vma(vmi, mpnt) {
struct file *file;
vma_start_write(mpnt);
if (mpnt->vm_flags & VM_DONTCOPY) {
retval = vma_iter_clear_gfp(&vmi, mpnt->vm_start,
mpnt->vm_end, GFP_KERNEL);
if (retval)
goto loop_out;
vm_stat_account(mm, mpnt->vm_flags, -vma_pages(mpnt));
continue;
}
charge = 0;
/*
* Don't duplicate many vmas if we've been oom-killed (for
* example)
*/
if (fatal_signal_pending(current)) {
retval = -EINTR;
goto loop_out;
}
if (mpnt->vm_flags & VM_ACCOUNT) {
unsigned long len = vma_pages(mpnt);
if (security_vm_enough_memory_mm(oldmm, len)) /* sic */
goto fail_nomem;
charge = len;
}
tmp = vm_area_dup(mpnt);
if (!tmp)
goto fail_nomem;
retval = vma_dup_policy(mpnt, tmp);
if (retval)
goto fail_nomem_policy;
tmp->vm_mm = mm;
retval = dup_userfaultfd(tmp, &uf);
if (retval)
goto fail_nomem_anon_vma_fork;
// 如果 VMA 标记为 VM_WIPEONFORK,则不复制匿名内存。
if (tmp->vm_flags & VM_WIPEONFORK) {
/*
* VM_WIPEONFORK gets a clean slate in the child.
* Don't prepare anon_vma until fault since we don't
* copy page for current vma.
*/
tmp->anon_vma = NULL;
}
// 否则,调用 anon_vma_fork 共享匿名内存的 anon_vma。
else if (anon_vma_fork(tmp, mpnt))
goto fail_nomem_anon_vma_fork;
vm_flags_clear(tmp, VM_LOCKED_MASK);
/*
* Copy/update hugetlb private vma information.
*/
if (is_vm_hugetlb_page(tmp))
hugetlb_dup_vma_private(tmp);
/*
* Link the vma into the MT. After using __mt_dup(), memory
* allocation is not necessary here, so it cannot fail.
*/
vma_iter_bulk_store(&vmi, tmp);
mm->map_count++;
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
// 如果 VMA 关联了文件,增加文件的引用计数,并将新的 VMA 插入文件的映射树。
file = tmp->vm_file;
if (file) {
struct address_space *mapping = file->f_mapping;
get_file(file);
i_mmap_lock_write(mapping);
if (vma_is_shared_maywrite(tmp))
mapping_allow_writable(mapping);
flush_dcache_mmap_lock(mapping);
/* insert tmp into the share list, just after mpnt */
vma_interval_tree_insert_after(tmp, mpnt,
&mapping->i_mmap);
flush_dcache_mmap_unlock(mapping);
i_mmap_unlock_write(mapping);
}
if (!(tmp->vm_flags & VM_WIPEONFORK))
retval = copy_page_range(tmp, mpnt);
if (retval) {
mpnt = vma_next(&vmi);
goto loop_out;
}
}
/* a new mm has just been created */
retval = arch_dup_mmap(oldmm, mm);
loop_out:
vma_iter_free(&vmi);
if (!retval) {
mt_set_in_rcu(vmi.mas.tree);
ksm_fork(mm, oldmm);
khugepaged_fork(mm, oldmm);
} else {
/*
* The entire maple tree has already been duplicated. If the
* mmap duplication fails, mark the failure point with
* XA_ZERO_ENTRY. In exit_mmap(), if this marker is encountered,
* stop releasing VMAs that have not been duplicated after this
* point.
*/
if (mpnt) {
mas_set_range(&vmi.mas, mpnt->vm_start, mpnt->vm_end - 1);
mas_store(&vmi.mas, XA_ZERO_ENTRY);
/* Avoid OOM iterating a broken tree */
set_bit(MMF_OOM_SKIP, &mm->flags);
}
/*
* The mm_struct is going to exit, but the locks will be dropped
* first. Set the mm_struct as unstable is advisable as it is
* not fully initialised.
*/
set_bit(MMF_UNSTABLE, &mm->flags);
}
out:
mmap_write_unlock(mm);
flush_tlb_mm(oldmm);
mmap_write_unlock(oldmm);
if (!retval)
dup_userfaultfd_complete(&uf);
else
dup_userfaultfd_fail(&uf);
return retval;
fail_nomem_anon_vma_fork:
mpol_put(vma_policy(tmp));
fail_nomem_policy:
vm_area_free(tmp);
fail_nomem:
// 如果在复制过程中发生错误,释放已分配的资源并返回错误码。
retval = -ENOMEM;
vm_unacct_memory(charge);
goto loop_out;
}
7.6.3.1. 复制 mm_struct
的元数据
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_mmap()
dup_mm_exe_file(mm, oldmm);
mm->total_vm = oldmm->total_vm;
mm->data_vm = oldmm->data_vm;
mm->exec_vm = oldmm->exec_vm;
mm->stack_vm = oldmm->stack_vm;
- 复制父进程的
mm_struct
元数据,包括虚拟内存大小、数据段大小、代码段大小和栈大小。
7.6.3.2. 复制虚拟内存区域(VMA)映射树
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_mmap()
retval = __mt_dup(&oldmm->mm_mt, &mm->mm_mt, GFP_KERNEL);
if (unlikely(retval))
goto out;
...
for_each_vma(vmi, mpnt) {
...
tmp = vm_area_dup(mpnt);
if (!tmp)
goto fail_nomem;
retval = vma_dup_policy(mpnt, tmp);
if (retval)
goto fail_nomem_policy;
tmp->vm_mm = mm;
...
}
- 调用
__mt_dup
复制父进程的虚拟内存区域(VMA
)映射树。 - 遍历父进程的每个
VMA
。 - 调用
vm_area_dup
复制VMA
的元数据。 - 调用
vma_dup_policy
复制VMA
的内存策略。
7.6.3.3. 复制页表项(写时拷贝)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_mmap()
if (!(tmp->vm_flags & VM_WIPEONFORK))
retval = copy_page_range(tmp, mpnt);
- 调用
copy_page_range
实现写时拷贝:- 页面表不会被直接复制,而是标记为只读。
- 当父进程或子进程尝试写入这些页面时,会触发页面错误(
Page Fault
),然后内核为写入的进程分配新的物理页面。
本文由于篇幅关系这边暂时不过多分析 copy_page_range
函数实现,此处简要描述该函数实现写时拷贝(COW
)这一机制的原理:
-
- 拷贝页表项:
copy_page_range
会遍历父进程的虚拟内存区域(VMA
)对应的页表,将父进程的页表项复制到子进程的页表中。(按照x86_64
的五级页表结构PGD
、P4D
、PUD
、PMD
、PTE
顺序逐级递归复制页表项)- 页表项中会标记页面为只读(通过设置页表项的权限位)。
-
- 实现写时拷贝(
COW
):
- 页面被标记为只读后,父进程和子进程共享相同的物理页面。
- 当父进程或子进程尝试写入这些页面时,会触发页面错误(
Page Fault
)。 - 页面错误处理程序会为写入的进程分配一个新的物理页面,并将原页面的内容复制到新页面中。
- 实现写时拷贝(
-
- 延迟实际的页面分配:
copy_page_range
不会立即分配新的物理页面,而是通过写时拷贝机制延迟页面分配,从而提高fork
的性能。
到这里也就实现了 fork()
函数的写时拷贝(COW
)机制。
7.6.4. 写时拷贝的体现
最终调用到 copy_page_range
函数只会复制父进程的页表项,并将子进程的页表项标记为只读权限,而不直接拷贝物理页面给子进程。当子进程尝试写入页面时,由于页面被标记为只读,从而触发 Page Fault
,这时陷入页面错误处理程序,才开始为子进程分配新的物理页面,并将之前的只读页面内容复制到新页面中。也就是说无需再创建过程中拷贝内存(物理页面的信息),通过 copy_mm
,子进程能够继承或共享父进程的内存布局,从而实现高效的进程创建。将实际拷贝推迟到真正发生写入时,这也便完成了写时拷贝这一机制。下图为 x86_64
下的 5
级页表转换查找物理地址示意图,在子进程与父进程共享内存时,将会查找到同一块物理页面,只不过该段内存子进程被标记为只读权限。
[注]:原画较大,点击图片查看高清图。
7.7. copy_process() 的核心函数 dup_task_struct()
dup_task_struct
函数作为 copy_process()
中一个核心函数,用来为新进程分配并初始化一个新的 task_struct
(任务结构)。在这里为新进程分配进程描述符 task_struct
和进程内核栈 kernel stack
。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
struct task_struct *tsk;
int err;
if (node == NUMA_NO_NODE)
node = tsk_fork_get_node(orig);
// 分配 task_struct
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL;
// 复制原始 task_struct 的内容
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_tsk;
// 分配内核栈
err = alloc_thread_stack_node(tsk, node);
if (err)
goto free_tsk;
// 如果启用了 CONFIG_THREAD_INFO_IN_TASK 配置(默认开启),表示 thread_info 与内核栈分离,包含进 task_struct
#ifdef CONFIG_THREAD_INFO_IN_TASK
// 初始化内核栈的引用计数为 1。
refcount_set(&tsk->stack_refcount, 1);
#endif
// 调用 account_kernel_stack 记录新任务的内核栈内存使用情况。
account_kernel_stack(tsk, 1);
err = scs_prepare(tsk, node);
if (err)
goto free_stack;
#ifdef CONFIG_SECCOMP
/*
* We must handle setting up seccomp filters once we're under
* the sighand lock in case orig has changed between now and
* then. Until then, filter must be NULL to avoid messing up
* the usage counts on the error path calling free_task.
*/
tsk->seccomp.filter = NULL;
#endif
// 初始化新任务的线程栈,复制原任务的线程信息。
setup_thread_stack(tsk, orig);
// 清除用户返回通知标志。
clear_user_return_notifier(tsk);
// 清除任务的重新调度标志。
clear_tsk_need_resched(tsk);
// 设置栈末尾的魔数,用于检测栈溢出。
set_task_stack_end_magic(tsk);
// 清除系统调用用户调度标志。
clear_syscall_work_syscall_user_dispatch(tsk);
// 如果启用了栈保护(CONFIG_STACKPROTECTOR),为新任务生成一个随机的栈保护值(stack_canary)。
#ifdef CONFIG_STACKPROTECTOR
tsk->stack_canary = get_random_canary();
#endif
// 如果原任务的 CPU 掩码指针指向其自身的 cpus_mask,则新任务的指针也指向自身的 cpus_mask。
if (orig->cpus_ptr == &orig->cpus_mask)
tsk->cpus_ptr = &tsk->cpus_mask;
// 调用 dup_user_cpus_ptr 复制用户定义的 CPU 掩码。
dup_user_cpus_ptr(tsk, orig, node);
/*
* One for the user space visible state that goes away when reaped.
* One for the scheduler.
*/
/*
* 初始化新任务的引用计数:
* rcu_users:RCU(Read-Copy-Update)用户计数,初始值为 2。
* usage:任务的使用计数,初始值为 1。
*/
refcount_set(&tsk->rcu_users, 2);
/* One for the rcu users */
refcount_set(&tsk->usage, 1);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
tsk->task_frag.page = NULL;
tsk->wake_q.next = NULL;
tsk->worker_private = NULL;
// 初始化内核代码覆盖率跟踪(kcov)。
kcov_task_init(tsk);
// 初始化内核内存安全分析(kmsan)。
kmsan_task_create(tsk);
// 初始化局部内核映射。
kmap_local_fork(tsk);
#ifdef CONFIG_FAULT_INJECTION
tsk->fail_nth = 0;
#endif
#ifdef CONFIG_BLK_CGROUP
tsk->throttle_disk = NULL;
tsk->use_memdelay = 0;
#endif
#ifdef CONFIG_ARCH_HAS_CPU_PASID
tsk->pasid_activated = 0;
#endif
#ifdef CONFIG_MEMCG
tsk->active_memcg = NULL;
#endif
#ifdef CONFIG_X86_BUS_LOCK_DETECT
tsk->reported_split_lock = 0;
#endif
#ifdef CONFIG_SCHED_MM_CID
tsk->mm_cid = -1;
tsk->last_mm_cid = -1;
tsk->mm_cid_active = 0;
tsk->migrate_from_cpu = -1;
#endif
// 如果所有初始化成功,返回新分配的 task_struct。
return tsk;
// 错误处理部分
free_stack:
// 调用 exit_task_stack_account 和 free_thread_stack 释放线程栈
exit_task_stack_account(tsk);
free_thread_stack(tsk);
free_tsk:
// 调用 free_task_struct 释放任务结构。
free_task_struct(tsk);
return NULL;
}
7.7.1. 参数及返回值
-
参数:
orig
:指向当前进程的task_struct
,用于作为模板复制。node
:NUMA
节点,用于指定在哪个内存节点上分配新任务结构。
-
返回值:
- 成功时返回新分配的
task_struct
指针。 - 失败时返回
NULL
。
- 成功时返回新分配的
7.7.2. 确定 NUMA 节点
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
if (node == NUMA_NO_NODE)
node = tsk_fork_get_node(orig);
- 作用:
- 如果未指定
NUMA
节点(node == NUMA_NO_NODE
),调用tsk_fork_get_node
根据当前进程的NUMA
策略选择一个节点。
- 如果未指定
7.7.3. 分配进程描述符 task_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL;
- 作用:
- 调用
alloc_task_struct_node
在指定的NUMA
节点上分配一个新的task_struct
。 - 如果分配失败,返回
NULL
。
- 调用
7.7.4. 复制原始任务结构
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_tsk;
- 作用:
- 调用
arch_dup_task_struct
将原始任务结构orig
的内容复制到新分配的tsk
中。 - 这是一个架构相关的函数,会根据具体的硬件架构执行额外的初始化。
- 如果复制失败,跳转到
free_tsk
标签释放已分配的资源。
- 调用
7.7.5. 分配进程内核栈
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
err = alloc_thread_stack_node(tsk, node);
if (err)
goto free_tsk;
- 作用:
- 调用
alloc_thread_stack_node
为新任务分配内核栈。 - 在
CONFIG_THREAD_INFO_IN_TASK
宏开启的情况下,thread_info
不再嵌套在内核栈中,因此栈的分配和task_struct
是分离的。 - 如果分配失败,跳转到
free_tsk
标签释放已分配的资源。
- 调用
7.7.6. 初始化 Shadow Call Stack
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
err = scs_prepare(tsk, node);
if (err)
goto free_stack;
- 作用:
- 调用
scs_prepare
初始化影子调用栈(SCS
,Shadow Call Stack
)。 - 如果初始化失败,跳转到
free_stack
标签释放线程栈。
- 调用
接下来初始化新任务的各种字段和子系统(如栈保护、CPU 掩码、引用计数等)。若所有步骤都成功完成则最终返回申请并初始化的 task_struct
。
7.8. 分配进程描述符 alloc_task_struct_node()
这里 alloc_task_struct_node()
作为 dup_task_struct()
的核心部分用于从内核的 slab
缓存上分配一个内存来创建新的 task_struct
(进程描述符)。 alloc_task_struct_node
是一个内联函数,实现如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static inline struct task_struct *alloc_task_struct_node(int node)
{
return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}
alloc_task_struct_node
函数只有一个参数 node
,用于指定从 NUMA
节点,即从指定的 NUMA
节点分配内存。接下来该函数直接调用一个 kmem_cache_alloc_node
从 slab
缓冲池中的内存分配函数就返回退出。那么接下来去看看 kmem_cache_alloc_node
函数的实现过程。
7.8.1. 调用 kmem_cache_alloc_node
参数描述:
kmem_cache_alloc_node
是内核提供的一个函数,用于从指定的slab
缓存中分配内存。- 参数说明:
task_struct_cachep
:- 这是一个全局变量,表示
task_struct
的slab
缓存池。 - 它在内核初始化时通过
kmem_cache_create_usercopy
创建。
- 这是一个全局变量,表示
GFP_KERNEL
:- 分配内存时使用的标志,表示分配内存时可以睡眠。
- 这是内核中最常用的分配标志,适用于大多数场景。
node
:- 指定从哪个
NUMA
节点分配内存。 - 如果系统支持
NUMA
(非一致性内存访问),可以通过该参数优化内存分配的局部性。
- 指定从哪个
这里简单解释一下全局变量 task_struct_cachep 的初始化过程,该全局变量用于管理 task_struct
的 slab
缓存。在内核启动的早期阶段的初始化进程管理子系统时调用 kmem_cache_create_usercopy
函数创建了该 slab
缓存,具体调用流程如下图。
在 fork_init
函数中通过 kmem_cache_create_usercopy
初始化:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
task_struct_cachep = kmem_cache_create_usercopy("task_struct",
arch_task_struct_size, align,
SLAB_PANIC|SLAB_ACCOUNT,
useroffset, usersize, NULL);
参数说明:
"task_struct"
: 缓存的名称。arch_task_struct_size
:task_struct
的大小,由架构定义。align
: 对齐要求,通常是L1_CACHE_BYTES
。SLAB_PANIC|SLAB_ACCOUNT
: 分配标志,表示分配失败时触发panic
,并启用内存计费。useroffset
和usersize
: 用于用户空间访问的偏移和大小。
7.8.2. kmem_cache_alloc_node 函数分析
函数定义:
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/slab.h
void *kmem_cache_alloc_node_noprof(struct kmem_cache *s, gfp_t flags, int node)
__assume_slab_alignment __malloc;
#define kmem_cache_alloc_node(...) alloc_hooks(kmem_cache_alloc_node_noprof(__VA_ARGS__))
kmem_cache_alloc_node
是一个宏,最终调用的是kmem_cache_alloc_node_noprof
。kmem_cache_alloc_node_noprof
是实际的实现函数。alloc_hooks
是一个宏,用于在内存分配函数的调用中插入额外的钩子逻辑。这些钩子通常用于调试、性能分析或其他扩展功能。
7.8.3. kmem_cache_alloc_node_noprof 函数分析
// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
void *kmem_cache_alloc_node_noprof(struct kmem_cache *s, gfp_t gfpflags, int node)
{
// slab_alloc_node 是核心分配函数,负责从指定的 kmem_cache 中分配对象。
void *ret = slab_alloc_node(s, NULL, gfpflags, node, _RET_IP_, s->object_size);
// 调用 trace_kmem_cache_alloc 跟踪分配事件。该跟踪函数用于调试和性能分析,帮助开发者了解分配行为。
trace_kmem_cache_alloc(_RET_IP_, ret, s, gfpflags, node);
return ret;
}
EXPORT_SYMBOL(kmem_cache_alloc_node_noprof);
该函数的核心业务是调用 slab_alloc_node
函数从 kmem_cache
即这里的 *s
中分配内存,然后调用 trace_kmem_cache_alloc
跟踪分配事件后返回分配的内存对象指针。
7.8.3. slab_alloc_node 函数分析
// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
static __fastpath_inline void *slab_alloc_node(struct kmem_cache *s, struct list_lru *lru,
gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
void *object;
bool init = false;
// 检查分配前的条件,例如是否需要触发内存分配失败注入(should_failslab)。
s = slab_pre_alloc_hook(s, gfpflags);
if (unlikely(!s))
return NULL;
// 调用 kfence_alloc 检查是否需要从 KFENCE(Kernel Electric Fence)分配内存。
object = kfence_alloc(s, orig_size, gfpflags);
if (unlikely(object))
goto out;
// 进入核心分配逻辑,尝试从指定的 slab 缓存中分配对象。
object = __slab_alloc_node(s, gfpflags, node, addr, orig_size);
// 如果启用了内存初始化选项(如 slab_want_init_on_free),清除对象中的空闲指针以防止数据泄露。
maybe_wipe_obj_freeptr(s, object);
// 检查是否需要初始化分配的对象
init = slab_want_init_on_alloc(gfpflags, s);
out:
/*
* When init equals 'true', like for kzalloc() family, only
* @orig_size bytes might be zeroed instead of s->object_size
* In case this fails due to memcg_slab_post_alloc_hook(),
* object is set to NULL
*/
/*
* 执行分配后的钩子操作,包括:
* * 内存初始化(如清零)。
* * 调用内存控制组(memcg)相关的分配钩子。
* * 调用 KASAN(Kernel Address Sanitizer)相关的分配钩子。
*/
slab_post_alloc_hook(s, lru, gfpflags, 1, &object, init, orig_size);
// 返回分配的对象指针。
return object;
}
// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
// Function: kmem_cache_alloc_node_noprof()
void *ret = slab_alloc_node(s, NULL, gfpflags, node, _RET_IP_, s->object_size);
传入参数说明:
s
: 指向分配对象的kmem_cache
;NULL
: 传递的list_lru
参数为NULL
,表示不涉及LRU
列表;gfpflags
: 分配标志,控制分配行为(如是否允许阻塞、是否从高优先级内存分配等);node
: 指定的NUMA
节点;_RET_IP_
: 调用者的返回地址,用于跟踪分配来源;s->object_size
: 分配对象的大小;
在当前函数中的核心业务是调用 __slab_alloc_node
函数从指定的 slab
缓存中分配对象。
7.8.4. __slab_alloc_node 函数分析
// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
static __always_inline void *__slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
struct kmem_cache_cpu *c;
struct slab *slab;
unsigned long tid;
void *object;
redo:
// 获取当前 CPU 的 kmem_cache_cpu 结构体。
c = raw_cpu_ptr(s->cpu_slab);
// tid 是事务 ID,用于确保分配操作的原子性。
tid = READ_ONCE(c->tid);
barrier();
// 快速路径尝试从当前 CPU 的 slab 缓存中分配对象。
object = c->freelist;
slab = c->slab;
#ifdef CONFIG_NUMA
if (static_branch_unlikely(&strict_numa) &&
node == NUMA_NO_NODE) {
struct mempolicy *mpol = current->mempolicy;
if (mpol) {
/*
* Special BIND rule support. If existing slab
* is in permitted set then do not redirect
* to a particular node.
* Otherwise we apply the memory policy to get
* the node we need to allocate on.
*/
if (mpol->mode != MPOL_BIND || !slab ||
!node_isset(slab_nid(slab), mpol->nodes))
node = mempolicy_slab_node();
}
}
#endif
// 如果 freelist 和 slab 都非空,则尝试分配。
// 并且如果当前 slab 不匹配指定的 NUMA 节点,或者 freelist 为空,则进入慢速路径。
if (!USE_LOCKLESS_FAST_PATH() ||
unlikely(!object || !slab || !node_match(slab, node))) {
// 调用 __slab_alloc 处理慢速路径分配。
object = __slab_alloc(s, gfpflags, node, addr, c, orig_size);
} else {
// 使用 get_freepointer_safe 获取下一个空闲对象。
void *next_object = get_freepointer_safe(s, object);
/*
* The cmpxchg will only match if there was no additional
* operation and if we are on the right processor.
*
* The cmpxchg does the following atomically (without lock
* semantics!)
* 1. Relocate first pointer to the current per cpu area.
* 2. Verify that tid and freelist have not been changed
* 3. If they were not changed replace tid and freelist
*
* Since this is without lock semantics the protection is only
* against code executing on this cpu *not* from access by
* other cpus.
*/
// 使用 cmpxchg 原子操作更新 freelist 和 tid。
if (unlikely(!__update_cpu_freelist_fast(s, object, next_object, tid))) {
// 如果 cmpxchg 失败,说明发生了竞争,重新尝试分配。
note_cmpxchg_failure("slab_alloc", s, tid);
goto redo;
}
prefetch_freepointer(s, next_object);
stat(s, ALLOC_FASTPATH);
}
// 如果分配成功,返回分配的对象指针。
return object;
}
__slab_alloc_node
是 SLUB
分配器的核心函数之一,用于从指定的 slab
缓存中分配对象。支持 NUMA
节点感知,并结合快速路径和慢速路径优化分配性能。
参数说明:
s
: 指向分配对象的kmem_cache
。gfpflags
: 分配标志,控制分配行为(如是否允许阻塞、是否从高优先级内存分配等)。node
: 指定的 NUMA 节点。addr
: 调用者的返回地址,用于跟踪分配来源。orig_size
: 原始分配大小。
实现细节
-
- 获取当前 CPU 的
kmem_cache_cpu
结构体;
- 获取当前 CPU 的
-
- 检查快速路径,如果
freelist
和slab
都非空,并slab
匹配指定的NUMA
节点,则进入快速路径分配;
- 检查快速路径,如果
-
- 否则调用
__slab_alloc
处理慢速路径分配;
- 否则调用
-
- 快速路径:使用
get_freepointer_safe
直接获取下一个slab
缓冲池中空闲对象(获取失败则尝试重新分配);
- 快速路径:使用
-
- 慢速路径:调用
__slab_alloc
找到合适的slab
(若没有则新分配一个slab
添加进当前CPU
的slab
中)并返回;
- 慢速路径:调用
-
- 返回分配的内存对象指针。
到此 dup_task_struct
函数就获得了一个新的 task_struct
内存对象。在创建新进程中分配新的 task_struct
内存空间的工作流程就如下图所示。
7.9. 分配进程内核栈 alloc_thread_stack_node()
在 kernel/fork.c
中有多个 alloc_thread_stack_node
函数定义,分别使用 THREAD_SIZE
、CONFIG_THREAD_INFO_IN_TASK
、CONFIG_VMAP_STACK
做直接或间接区别。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
# if THREAD_SIZE >= PAGE_SIZE || defined(CONFIG_VMAP_STACK)
# ifdef CONFIG_VMAP_STACK
在之前的文章开始笔者就已经介绍了 THREAD_SIZE
在当前系统 x86_64
下是 16KB
,而 PAGE_SIZE
是 4KB
。且 CONFIG_VMAP_STACK
宏是开启的。因此,这里的调用的 alloc_thread_stack_node
函数实现如下,需要注意的是,此时进程描述符已经通过 alloc_task_struct_node
分配好了。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// 每个 CPU 的缓存栈数量
#define NR_CACHED_STACKS 2
static int alloc_thread_stack_node(struct task_struct *tsk, int node)
{
// 用于存储分配的栈的虚拟内存区域(vm_struct)。
struct vm_struct *vm;
// 指向分配的栈的起始地址。
void *stack;
// 用于循环遍历每 CPU 栈缓存。
int i;
// 尝试从每 CPU 栈缓存中获取栈
for (i = 0; i < NR_CACHED_STACKS; i++) {
struct vm_struct *s;
s = this_cpu_xchg(cached_stacks[i], NULL);
if (!s)
continue;
/* Reset stack metadata. */
// 重置栈的元数据
kasan_unpoison_range(s->addr, THREAD_SIZE);
stack = kasan_reset_tag(s->addr);
/* Clear stale pointers from reused stack. */
// 清除栈中的旧数据
memset(stack, 0, THREAD_SIZE);
// 为栈进行内存控制组(memcg)计费
if (memcg_charge_kernel_stack(s)) {
vfree(s->addr);
return -ENOMEM;
}
tsk->stack_vm_area = s;
tsk->stack = stack;
return 0;
}
/*
* Allocated stacks are cached and later reused by new threads,
* so memcg accounting is performed manually on assigning/releasing
* stacks to tasks. Drop __GFP_ACCOUNT.
*/
// 如果缓存中没有可用栈,则分配新的栈
stack = __vmalloc_node(THREAD_SIZE, THREAD_ALIGN,
THREADINFO_GFP & ~__GFP_ACCOUNT,
node, __builtin_return_address(0));
if (!stack)
return -ENOMEM;
vm = find_vm_area(stack);
if (memcg_charge_kernel_stack(vm)) {
vfree(stack);
return -ENOMEM;
}
/*
* We can't call find_vm_area() in interrupt context, and
* free_thread_stack() can be called in interrupt context,
* so cache the vm_struct.
*/
// 缓存分配的栈的元数据
tsk->stack_vm_area = vm;
stack = kasan_reset_tag(stack);
tsk->stack = stack;
return 0;
}
7.9.1. 参数说明
struct task_struct *tsk
:- 指向需要为其分配内核栈的进程描述符的指针。(此时指向已分配好的进程描述符)
int node
:- 指定分配内核栈时的 NUMA 节点。
7.9.2. 从每 CPU 栈缓存中获取栈
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
for (i = 0; i < NR_CACHED_STACKS; i++) {
struct vm_struct *s;
s = this_cpu_xchg(cached_stacks[i], NULL);
if (!s)
continue;
- 目的:尝试从当前
CPU
的栈缓存(cached_stacks
)中获取一个可用的栈。 - 实现:
- 使用
this_cpu_xchg
从当前CPU
的cached_stacks
中取出一个栈(vm_struct
)。(每个CPU
有两个缓存栈) - 如果缓存槽为空(
s == NULL
),则跳过当前槽,继续检查下一个缓存槽。
- 使用
- cached_stacks:每个
CPU
的内核缓存栈。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
#define NR_CACHED_STACKS 2
static DEFINE_PER_CPU(struct vm_struct *, cached_stacks[NR_CACHED_STACKS]);
// Function:fork_init()
#ifdef CONFIG_VMAP_STACK
cpuhp_setup_state(CPUHP_BP_PREPARE_DYN, "fork:vm_stack_cache",
NULL, free_vm_stack_cache);
#endif
-
cpuhp_setup_state
:- 注册了一个
CPU
热插拔状态处理函数。 - 在
CPU
启动时,free_vm_stack_cache
函数会被调用,用于清理每CPU
的cached_stacks
。
- 注册了一个
-
cached_stacks
的初始化:DEFINE_PER_CPU
宏定义的每CPU
的cached_stacks
在内核启动时会被自动分配并初始化为NULL
。- 因此,
cached_stacks
的每个槽在内核启动时默认是空的。
cached_stacks
的初始化发生在内核启动阶段的 fork_init
函数中,当 CONFIG_VMAP_STACK
宏被启用时。
7.9.3. 重置栈的元数据
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
/* Reset stack metadata. */
kasan_unpoison_range(s->addr, THREAD_SIZE);
stack = kasan_reset_tag(s->addr);
/* Clear stale pointers from reused stack. */
memset(stack, 0, THREAD_SIZE);
- 目的:确保从缓存中取出的栈可以安全使用。
- 实现:
- 调用
kasan_unpoison_range
:如果启用了KASAN
(Kernel Address Sanitizer
),则重置栈的元数据。 - 调用
kasan_reset_tag
:重置栈的标记(如果启用了KASAN
)。 - 调用
memset
:将栈清零,清除可能的残留数据。
- 调用
7.9.4. 为栈进行内存控制组(memcg)计费
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
if (memcg_charge_kernel_stack(s)) {
vfree(s->addr);
return -ENOMEM;
}
- 目的:为分配的栈进行内存计费,确保其符合内存控制组的限制。
- 实现:
- 调用
memcg_charge_kernel_stack
为栈计费。 - 如果计费失败,则释放栈并返回错误。
- 调用
7.9.5. 更新任务的栈指针
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
tsk->stack_vm_area = s;
tsk->stack = stack;
return 0;
- 目的:将分配的栈与任务(
task_struct
)关联。 - 实现:
- 将栈的元数据(
vm_struct
)存储到tsk->stack_vm_area
。 - 将栈的起始地址存储到
tsk->stack
。 - 返回成功。
- 将栈的元数据(
7.9.6. 分配新的栈
如果缓存中没有可用栈,则分配新的栈:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
stack = __vmalloc_node(THREAD_SIZE, THREAD_ALIGN,
THREADINFO_GFP & ~__GFP_ACCOUNT,
node, __builtin_return_address(0));
if (!stack)
return -ENOMEM;
- 目的:当缓存中没有可用栈时,分配新的栈。
- 实现:
- 调用
__vmalloc_node
分配一块大小为THREAD_SIZE
的内存,分配对齐方式为THREAD_ALIGN
。 - 使用
THREADINFO_GFP & ~__GFP_ACCOUNT
作为分配标志,移除了__GFP_ACCOUNT
,因为内存计费是手动完成的。 - 如果分配失败,则返回
-ENOMEM
。
- 调用
7.9.7. 查找虚拟内存区域
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
vm = find_vm_area(stack);
if (memcg_charge_kernel_stack(vm)) {
vfree(stack);
return -ENOMEM;
}
- 目的:获取分配的栈对应的虚拟内存区域(
vm_struct
)。 - 实现:
- 调用
find_vm_area
获取栈的vm_struct
。 - 调用
memcg_charge_kernel_stack
为栈计费。 - 如果计费失败,则释放栈并返回错误。
- 调用
7.9.8. 更新任务的栈指针
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:alloc_thread_stack_node()
tsk->stack_vm_area = vm;
stack = kasan_reset_tag(stack);
tsk->stack = stack;
return 0;
- 目的:将分配的栈与任务(
task_struct
)关联。 - 实现:
- 将栈的元数据(
vm_struct
)存储到tsk->stack_vm_area
。 - 将栈的起始地址存储到
tsk->stack
。 - 返回成功。
- 将栈的元数据(
alloc_thread_stack_node
函数 优先使用缓存栈( cached stack
),通过每 CPU
栈缓存减少频繁分配和释放栈的开销。当缓存中没有可用栈时,通过 vmalloc
分配新的栈。到此进程的内核栈也就分配结束了,接下来只需要继续完成其它初始化工作,就可以将该进程唤醒加入内核任务调度列表了准备上处理机运行了。那么这时候就可以看到 dup_task_struct
函数的工作全貌了。
7.10. fork() 函数的简要总体框图
通过上文的分析,到这里我们也大致掌握了 fork()
函数的工作流程,也就是说你已经清楚了一个进程是如何被创建出来的,笔者将其关键流程简要画了出来以供大家梳理。
8. exit() 进程退出
在了解完进程的创建流程后,我们也应该学习一下一个进程是如何退出操作系统的。相信大家都知道 exit()
系统调用,就是用来终止一个进程。它会清理进程的资源、通知父进程,并最终将进程从系统中移除。接下来就来仔细分析该函数。
8.1. exit() 系统调用的定义
exit()
系统调用的实现如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
SYSCALL_DEFINE1(exit, int, error_code)
{
do_exit((error_code&0xff)<<8);
}
-
函数定义:
SYSCALL_DEFINE1
是一个宏,用于定义一个系统调用。表示exit
系统调用接受一个参数error_code
,表示进程的退出状态。
-
参数处理:
error_code & 0xff
:将error_code
的低 8 位提取出来。<< 8
:将提取的值左移8
位。这是因为在Linux
中,进程的退出状态通常存储在高16
位中,而低8
位用于其他信息。
-
调用
do_exit
:do_exit
是内核中实际处理进程退出的核心函数。它负责完成进程退出的所有工作,包括资源释放、通知父进程等。
8.2. do_exit() 函数
do_exit()
函数的实现位置:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// __noreturn 表示该函数不会返回
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
// 检查中断是否被禁用。如果中断被禁用,可能会导致系统不稳定。
WARN_ON(irqs_disabled());
// 确保线程组中的所有线程都同步退出。设置 SIGNAL_GROUP_EXIT 标志,并记录退出代码。
synchronize_group_exit(tsk, code);
WARN_ON(tsk->plug);
kcov_task_exit(tsk);
kmsan_task_exit(tsk);
coredump_task_exit(tsk);
ptrace_event(PTRACE_EVENT_EXIT, code);
user_events_exit(tsk);
io_uring_files_cancel();
exit_signals(tsk); /* sets PF_EXITING */
// 清理与 Seccomp 安全过滤器相关的资源。
seccomp_filter_release(tsk);
// 更新进程的资源使用统计信息。
acct_update_integrals(tsk);
// 检查线程组是否已终止
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/*
* If the last thread of global init has exited, panic
* immediately to get a useable coredump.
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);
#ifdef CONFIG_POSIX_TIMERS
hrtimer_cancel(&tsk->signal->real_timer);
exit_itimers(tsk);
#endif
if (tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}
// 调用 `acct_collect` 收集进程的资源使用信息。
acct_collect(code, group_dead);
// 如果 group_dead 成立,则表示进程组已经退出的话,就调用 tty_audit_exit 来更新当前进程的审计状态
if (group_dead)
tty_audit_exit();
// 调用 `audit_free` 清理审计相关的资源。
audit_free(tsk);
// 设置任务的状态码为传入的code
tsk->exit_code = code;
// 通过taskstats_exit来更新当前进程的任务统计信息,并告知用户进程空间。 接下来就到了释放资源的时候了。
taskstats_exit(tsk, group_dead);
exit_mm();
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
exit_sem(tsk);
exit_shm(tsk);
exit_files(tsk);
exit_fs(tsk);
if (group_dead)
disassociate_ctty(1);
exit_task_namespaces(tsk);
exit_task_work(tsk);
exit_thread(tsk);
/*
* Flush inherited counters to the parent - before the parent
* gets woken up by child-exit notifications.
*
* because of cgroup mode, must be called before cgroup_exit()
*/
perf_event_exit_task(tsk);
sched_autogroup_exit_task(tsk);
cgroup_exit(tsk);
/*
* FIXME: do that only when needed, using sched_exit tracepoint
*/
flush_ptrace_hw_breakpoint(tsk);
exit_tasks_rcu_start();
exit_notify(tsk, group_dead);
proc_exit_connector(tsk);
mpol_put_task_policy(tsk);
#ifdef CONFIG_FUTEX
if (unlikely(current->pi_state_cache))
kfree(current->pi_state_cache);
#endif
/*
* Make sure we are holding no locks:
*/
debug_check_no_locks_held();
if (tsk->io_context)
exit_io_context(tsk);
if (tsk->splice_pipe)
free_pipe_info(tsk->splice_pipe);
if (tsk->task_frag.page)
put_page(tsk->task_frag.page);
exit_task_stack_account(tsk);
check_stack_usage();
preempt_disable();
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
exit_rcu();
exit_tasks_rcu_finish();
lockdep_free_task(tsk);
do_task_dead();
}
8.2.0. do_exit() 的参数和返回值
- 参数
code
: 表示进程的退出状态,通常由exit
系统调用或其他导致进程终止的事件传递。 __noreturn
: 表示该函数不会返回。
8.2.1. 检查和同步线程组退出
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
WARN_ON(irqs_disabled());
synchronize_group_exit(tsk, code);
- 检查中断状态:确保中断未被禁用。
- 同步线程组退出:调用
synchronize_group_exit
,确保线程组的退出状态一致。如果这是线程组中最后一个线程退出,则设置SIGNAL_GROUP_EXIT
标志。
8.2.2. 清理与调试相关的资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
kcov_task_exit(tsk);
kmsan_task_exit(tsk);
coredump_task_exit(tsk);
ptrace_event(PTRACE_EVENT_EXIT, code);
user_events_exit(tsk);
kcov_task_exit
:清理与代码覆盖率跟踪相关的资源。- 通知
kcov
(Kernel Coverage
)子系统当前进程即将退出。 kcov
是内核中的代码覆盖率工具,用于跟踪内核代码的执行路径,通常用于内核的模糊测试。
- 通知
kmsan_task_exit
:清理与内存污点分析相关的资源。- 通知
KMSAN
(Kernel Memory Sanitizer
)子系统当前进程即将退出。 KMSAN
是一种内核工具,用于检测未初始化的内存使用。
- 通知
coredump_task_exit
:处理可能的核心转储。- 处理可能的核心转储(
core dump
)。 - 如果进程因信号(如
SIGSEGV
)退出,并且核心转储已启用,则生成核心转储文件。
- 处理可能的核心转储(
ptrace_event
:通知调试器进程退出事件。- 通知调试器(如
gdb
)当前进程即将退出。 ptrace
是一个系统调用,允许父进程监视和控制子进程的执行。
- 通知调试器(如
user_events_exit
:清理用户事件相关的资源。
8.2.3. 取消 I/O 和信号处理
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
io_uring_files_cancel();
exit_signals(tsk); /* sets PF_EXITING */
- 取消 I/O 请求:调用
io_uring_files_cancel
取消未完成的I/O
请求。 - 设置退出标志:调用
exit_signals
设置PF_EXITING
标志,表示进程正在退出。
8.2.4. 检查线程组是否已终止
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);
#ifdef CONFIG_POSIX_TIMERS
hrtimer_cancel(&tsk->signal->real_timer);
exit_itimers(tsk);
#endif
if (tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}
- 减少线程计数:通过
atomic_dec_and_test
检查线程组是否已完全退出。tsk->signal->live
信号原子减一,如果减完以后值为0
则返回真,会调用exit_itimers
释放掉进程的计时器相关内容,因为此时已经没有进程了。 - 特殊处理 init 进程:如果退出的是全局
init
进程(PID 1
),触发内核panic
。 - 释放定时器:
exit_itimers
取消POSIX
定时器。 - 更新内存使用统计:
setmax_mm_hiwater_rss
记录内存使用的高水位值。
8.2.5. 释放系统资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
exit_mm();
exit_sem(tsk);
exit_shm(tsk);
exit_files(tsk);
exit_fs(tsk);
if (group_dead)
disassociate_ctty(1);
- 释放内存管理资源:调用
exit_mm
释放进程的内存空间。 - 信号量和共享内存 :释放进程的信号量和共享内存资源(减少共享内存引用奇数或直接释放)。
- 关闭文件描述符:调用
exit_files
关闭所有打开的文件。 - 释放文件系统资源:调用
exit_fs
释放文件系统相关的资源。 - 解除终端关联:如果线程组已退出,
disassociate_ctty
解除与控制终端的关联。
8.2.6. 释放线程和调度相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
exit_task_work(tsk);
exit_thread(tsk);
perf_event_exit_task(tsk);
sched_autogroup_exit_task(tsk);
cgroup_exit(tsk);
- 执行挂起的任务工作:调用
exit_task_work
执行挂起的任务。 - 释放线程资源:调用
exit_thread
释放线程相关的资源。 - 释放性能事件资源:调用
perf_event_exit_task
清理性能事件相关的资源。 - 退出调度组和控制组:调用
sched_autogroup_exit_task
和cgroup_exit
。
8.2.7. 通知父进程
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
// 刷新当前进程的硬件断点信息
flush_ptrace_hw_breakpoint(tsk);
// 开启一个RCU临界区
exit_tasks_rcu_start();
// 较为关键的一个函数具体在下文中描述
exit_notify(tsk, group_dead);
proc_exit_connector(tsk);
- 通知父进程:调用
exit_notify
通知父进程当前进程已退出。- 寻找养父: 调用
forget_original_parent
函数给当前进程的子进程寻找新的养父,优先分配给同一进程组的其他进程。如果没有进程组成员,则分配给子进程重定向的父进程(child_subreaper
)。 - 设置当前进程退出状态:
exit_notify
会将当前进程的退出状态(task_struct.exit_state
)设置为僵尸状态EXIT_ZOMBIE
,表示进程已退出但尚未被父进程回收。 - 当进程设置为自动回收时: 即
exit_notify
中的autoreap
为true
时,当前进程的退出状态会从EXIT_ZOMBIE
更变为EXIT_DEAD
,表示进程已经完全退出,资源可以被释放。此时会执行list_add(&tsk->ptrace_entry, &dead);
当前进程会被加入到dead
列表中,稍后会调用release_task
函数释放dead
进程列表中进程的资源。(也就是当父进程不主动回收子进程时,子进程将在这里会被自动回收,有关release_task
函数的具体工作内容将在后面的文章中详细描述,此处暂时理解为会释放进程相关资源就好。)
[注]:笔者这里没有贴 exit_notify 函数的实现代码,其路径同样在 kernel/exit.c 中。
- 寻找养父: 调用
- 更新
/proc
文件系统:调用proc_exit_connector
更新进程的/proc
信息。向/proc
文件系统发出进程退出的事件通知
接下来依然是释放进程相关资源,如io
、pipe
、page
任务页资源,并更新当前进程栈资源统计信息
8.2.8. 减少进程栈引用计数,设置进程状态 DEAD 并切换进程
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_exit()
exit_task_stack_account(tsk);
check_stack_usage();
preempt_disable(); // 禁止抢占
// 如果进程有脏页的话,就把脏页加到CPU变量中,以后处理
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
exit_rcu();
exit_tasks_rcu_finish();
lockdep_free_task(tsk);
// 又一关键函数调用
do_task_dead();
- exit_task_stack_account: 减少内核栈的内存统计
-1
(如LRU
计数)。如果启用了CONFIG_VMAP_STACK
,遍历内核栈的每一页,调用memcg_kmem_uncharge_page
解除memcg
计费; - check_stack_usage:统计和记录当前进程实际使用的内核栈空间,用于调试和内核栈使用分析。(启用
CONFIG_DEBUG_STACK_USAGE
时有效) - preempt_disable:禁止内核抢占;
- RCU 清理:调用
exit_rcu
退出RCU
临界区和exit_tasks_rcu_finish
处理RCU
(Read-Copy-Update
)相关的清理。 - 释放任务锁定资源:调用
lockdep_free_task
。释放当前进程的锁依赖资源 - do_task_dead:
-
- 首先将任务(
task_struct.__state
)标记为TASK_DEAD
并从系统中移除。 通知内核当前进程已结束。
- 首先将任务(
-
- 调用
__schedule()
切换到另一个可运行的进程(这也就是do_exit
函数不会有返回值的原因)。在此刻,调度器检测到当前任务已不再是TASK_RUNNING
,因此不会再将其重新分配CPU
,真正终结了该进程的执行路径。任务以僵尸(Zombie
)形式保留在系统中,只保留必要的进程表信息(如exit_code
),不再占用用户空间内存或大部分内核资源。
- 调用
-
8.2.9. 进程消亡
此处代码与进程资源释放相关,不过由于本章内容重点不是学习进程调度,因此此处笔者将代码省略的贴了出来,仅包含其重点信息。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/sched/core.c
static void __sched notrace __schedule(int sched_mode)
{
struct task_struct *prev, *next;
...
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
// 检查调度前的各种状态(如栈溢出、原子上下文等),用于调试。
schedule_debug(prev, preempt);
if (sched_feat(HRTICK) || sched_feat(HRTICK_DL))
hrtick_clear(rq);
// 关闭本地中断。
local_irq_disable();
// 通知 RCU 发生上下文切换。
rcu_note_context_switch(preempt);
...
// 选择下一个要运行的任务(通常是优先级最高的 runnable 任务)。
next = pick_next_task(rq, prev, &rf);
rq_set_donor(rq, next);
...
/* Also unlocks the rq: */
// 执行实际的上下文切换,包括切换寄存器、栈、mm、fpu 等。
rq = context_switch(rq, prev, next, &rf);
...
}
以上代码可以简单的理解为关闭中断后开始寻找合适的待运行任务,然后调用 context_switch
函数进行上下文切换。其实现代码同样也是省略的贴出来如下。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/sched/core.c
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
// 调用各种钩子(如 perf、notifier、kmap、arch-specific),准备切换。
prepare_task_switch(rq, prev, next);
// 架构相关的上下文切换准备。
arch_start_context_switch(prev);
...
// 此段内容切换内存地址空间(mm_struct),处理 TLB、lazy TLB、membarrier 等。
...
// 释放 rq 锁,为下一个任务准备锁定环境。
prepare_lock_switch(rq, next, rf);
// 核心上下文切换,切换寄存器、栈、FPU、TLS、CPU 状态等。
switch_to(prev, next, prev);
barrier();
// 调用 finish_task_switch 清理相关内容,其中就包含内核栈和进程描述符
return finish_task_switch(prev);
}
// Linux Kernel 6.15.0-rc2
// PATH: kernel/sched/core.c
static struct rq *finish_task_switch(struct task_struct *prev)
__releases(rq->lock)
{
struct rq *rq = this_rq();
struct mm_struct *mm = rq->prev_mm;
unsigned int prev_state;
// 确保调度器的抢占计数(preempt_count)处于预期状态,防止调度器状态异常。
if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET,
"corrupted preempt_count: %s/%d/0x%x\n",
current->comm, current->pid, preempt_count()))
preempt_count_set(FORK_PREEMPT_COUNT);
rq->prev_mm = NULL;
// 读取 prev->__state(必须在 finish_task 之前读取,防止并发 wakeup 导致的竞态。)
prev_state = READ_ONCE(prev->__state);
vtime_task_switch(prev);
perf_event_task_sched_in(prev, current);
// 将 prev->on_cpu 置 0,表示 prev 已不在 CPU 上运行。
finish_task(prev);
...
// 如果 prev 已死亡(prev->__state == TASK_DEAD)
if (unlikely(prev_state == TASK_DEAD)) {
// 调用调度类的 task_dead 钩子(如有)
if (prev->sched_class->task_dead)
prev->sched_class->task_dead(prev);
// 调用 put_task_stack(prev) 释放内核栈。
put_task_stack(prev);
// 调用 put_task_struct_rcu_user(prev) 以 RCU 方式释放进程描述符。
put_task_struct_rcu_user(prev);
}
return rq;
}
此处仅看最后的条件分支,判断被切出去的进程状态是否是 TASK_DEAD
,若是则执行代码段内部代码,由于在之前的 do_task_dead
函数中已经首先将退出进程状态设置为 TASK_DEAD
,因此这里必然是会进该分支执行,此处有两个很关键的函数调用 :
- put_task_stack:将会注册
RCU
机制延时释放进程内核栈(延时到引用计数为0
时才释放); - put_task_struct_rcu_user:注册
RCU
机制延时释放进程描述符(延时到引用计数为0
时才释放)。
[注]:有关 put_task_struct_rcu_user 函数的梳理,将在本文 9.8. 小节展示。
8.2.10. put_task_stack() 释放内核栈
上述所调用的 put_task_stack
函数声明位置在 task_stack.h
中,其实现位置在 fork.c
中。
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched/task_stack.h
#ifdef CONFIG_THREAD_INFO_IN_TASK
extern void put_task_stack(struct task_struct *tsk);
#else
...
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
#ifdef CONFIG_THREAD_INFO_IN_TASK
void put_task_stack(struct task_struct *tsk)
{
if (refcount_dec_and_test(&tsk->stack_refcount))
release_task_stack(tsk);
}
#endif
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:put_task_stack()
if (refcount_dec_and_test(&tsk->stack_refcount))
- 对
tsk->stack_refcount
进行原子减1操作。 - 如果减完后计数为
0
,返回true
,表示没有其他引用在使用该内核栈。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:put_task_stack()
release_task_stack(tsk);
- 当引用计数为0时,调用
release_task_stack(tsk)
释放内核栈。 release_task_stack
内部会检查进程状态(必须为TASK_DEAD
),然后调用free_thread_stack(tsk)
进行实际的内核栈释放(如vfree
)。
8.2.11. release_task_stack()
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static void release_task_stack(struct task_struct *tsk)
{
if (WARN_ON(READ_ONCE(tsk->__state) != TASK_DEAD))
return; /* 防止在进程未完全退出时释放栈 */
free_thread_stack(tsk);
}
检查进程状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: release_task_stack()
if (WARN_ON(READ_ONCE(tsk->__state) != TASK_DEAD))
return;
- 确保进程的状态为
TASK_DEAD
,即进程已经完全退出。 - 如果进程未完全退出,则不会释放内核栈,并打印警告。
释放内核栈
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: release_task_stack()
free_thread_stack(tsk);
- 调用
free_thread_stack
函数释放内核栈的内存。
8.2.12. free_thread_stack()
free_thread_stack
的实现根据内核配置(如 CONFIG_VMAP_STACK
)有所不同。由于当前内核是默认开启 CONFIG_VMAP_STACK
,因此当前内核将会使用虚拟内存映射(vmalloc
)的方式分配内核栈,而非直接使用物理内存。这里的 free_thread_stack
函数实现如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static void free_thread_stack(struct task_struct *tsk)
{
if (!try_release_thread_stack_to_cache(tsk->stack_vm_area))
thread_stack_delayed_free(tsk);
tsk->stack = NULL;
tsk->stack_vm_area = NULL;
}
-
try_release_thread_stack_to_cache
:- 尝试将内核栈缓存起来以供后续复用。
- 如果无法缓存,则调用
thread_stack_delayed_free
延迟释放内核栈。
-
thread_stack_delayed_free
:- 使用
RCU
延迟机制释放内核栈,确保并发安全。
- 使用
8.2.13. thread_stack_delayed_free()
thread_stack_delayed_free
的实现根据内核配置(如 CONFIG_VMAP_STACK
)有所不同。thread_stack_delayed_free
函数用于延迟释放内核栈,确保在安全的上下文中释放虚拟内存。同样是在 CONFIG_VMAP_STACK
开启时的 thread_stack_delayed_free
函数实现:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static void thread_stack_delayed_free(struct task_struct *tsk)
{
struct vm_stack *vm_stack = tsk->stack;
vm_stack->stack_vm_area = tsk->stack_vm_area;
call_rcu(&vm_stack->rcu, thread_stack_free_rcu);
}
函数参数
struct task_struct *tsk
:- 指向需要释放内核栈的进程描述符。
获取vm_stack
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: thread_stack_delayed_free()
struct vm_stack *vm_stack = tsk->stack;
tsk->stack
是指向内核栈的指针。- 在
CONFIG_VMAP_STACK
宏开启时,tsk->stack
实际上是一个struct vm_stack
类型的指针。
设置 stack_vm_area
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: thread_stack_delayed_free()
vm_stack->stack_vm_area = tsk->stack_vm_area;
- 将
tsk->stack_vm_area
(指向虚拟内存区域的vm_struct
)赋值给vm_stack->stack_vm_area
。 - 这样可以在延迟释放时访问到虚拟内存区域的元数据。
延迟释放内核栈
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: thread_stack_delayed_free()
call_rcu(&vm_stack->rcu, thread_stack_free_rcu);
- 使用
RCU
(Read-Copy-Update
)机制延迟释放内核栈。 - 注册一个回调函数
thread_stack_free_rcu
,当RCU
的读操作完成后,内核会调用该回调函数释放内核栈。RCU
的回调函数会在RCU Grace Period
结束 时触发执行。Grace Period
结束的条件是:- 所有持有
rcu_read_lock
的读者释放了锁。 - 没有任何并发读操作正在访问受保护的资源。
- 所有持有
8.2.14. thread_stack_free_rcu()
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static void thread_stack_free_rcu(struct rcu_head *rh)
{
struct vm_stack *vm_stack = container_of(rh, struct vm_stack, rcu);
if (try_release_thread_stack_to_cache(vm_stack->stack_vm_area))
return;
vfree(vm_stack);
}
-
获取
vm_stack
// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c // Function: thread_stack_free_rcu() struct vm_stack *vm_stack = container_of(rh, struct vm_stack, rcu);
- 使用
container_of
宏从rcu_head
指针获取vm_stack
的地址。
- 使用
-
尝试将内核栈缓存起来
// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c // Function: thread_stack_free_rcu() if (try_release_thread_stack_to_cache(vm_stack->stack_vm_area)) return;
- 调用
try_release_thread_stack_to_cache
尝试将内核栈缓存起来,以供后续复用。 - 如果成功缓存,则直接返回,不释放内存。
- 调用
-
释放内核栈
// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c // Function: thread_stack_free_rcu() vfree(vm_stack);
- 如果无法缓存,则调用
vfree
释放虚拟内存。
- 如果无法缓存,则调用
8.2.15. try_release_thread_stack_to_cache()
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static bool try_release_thread_stack_to_cache(struct vm_struct *vm)
{
unsigned int i;
for (i = 0; i < NR_CACHED_STACKS; i++) {
struct vm_struct *tmp = NULL;
if (this_cpu_try_cmpxchg(cached_stacks[i], &tmp, vm))
return true;
}
return false;
}
-
遍历缓存池
// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c // Function: try_release_thread_stack_to_cache() for (i = 0; i < NR_CACHED_STACKS; i++) {
- 遍历当前
CPU
的内核栈缓存池(cached_stacks
)。
- 遍历当前
-
尝试将内核栈加入缓存
// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c // Function: try_release_thread_stack_to_cache() if (this_cpu_try_cmpxchg(cached_stacks[i], &tmp, vm)) return true;
- 使用
this_cpu_try_cmpxchg
尝试将当前内核栈(vm
)存入缓存池。 - 如果缓存池中对应位置为空(
tmp == NULL
),则将vm
存入,并返回true
。
- 使用
-
返回失败
// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c // Function: try_release_thread_stack_to_cache() return false;
- 如果缓存池已满,则返回
false
,表示无法缓存。
- 如果缓存池已满,则返回
使用 RCU
机制延迟释放内核栈,确保在所有可能的并发访问完成后再释放内存。在释放内核栈之前,尝试将其缓存起来以供后续复用。由于 vmalloc
和 vfree
操作相对较慢,尤其是在频繁分配和释放内核栈时,会导致性能下降。这种方式可以减少频繁的 vmalloc
和 vfree
调用,提高性能并减少 TLB
刷新开销。如果实在无法缓存内核栈,才调用 vfree
释放虚拟内存。到此一个进程的所有资源也就释放完毕
[注]:每次调用 vfree 时,都会触发 TLB(Translation Lookaside Buffer)刷新,以确保虚拟地址空间的正确性。
8.3. do_exit() 函数小结
当一个进程再没有其它进程引用时,或父进程再等待时,至此,那么该进程就已经终结了,并且所有进程资源全部都被释放(包括内核栈和进程描述符)。
但是注意当这个进程在父进程主动 wait
时,只是作为一个僵尸进程存在,并没有真正的消亡。它的实体 task_struct
也即进程描述符和进程内核栈仍然存在(因为还有进程正在引用,因此内核会延迟释放),僵尸进程的资源最终由其父进程在调用 wait()
、waitpid()
或相关变体时回收。内核此时调用 release_task()
来确切释放 task_struct
及剩余内核资源(包括进程描述符、内核栈等资源),彻底清除进程条目,完成进程生命周期最后一步,至此这个进程才算是真正的消亡了。
9. 阻塞等待退出
在实现多进程程序时,父进程通常会使用 wait
函数阻塞等待子进程退出以确保程序执行逻辑。而当子进程执行了 exit
系统调用后,此时父进程也就等待的是一个被标记为 EXIT_ZOMBIE
退出状态但并未回收的僵尸进程,此时 wait
系统调用就会主动寻找僵尸子进程并回收其资源。
9.1. wait 系统调用
wait
系统调用家族的实现如下:
wait4
系统调用:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr,
int, options, struct rusage __user *, ru)
{
struct rusage r;
long err = kernel_wait4(upid, stat_addr, options, ru ? &r : NULL);
if (err > 0) {
if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
return -EFAULT;
}
return err;
}
- 定义位置:
kernel/exit.c
- 功能:实现
wait4
系统调用,用于等待特定子进程的状态变化。
waitpid
系统调用:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
SYSCALL_DEFINE3(waitpid, pid_t, pid, int __user *, stat_addr, int, options)
{
return kernel_wait4(pid, stat_addr, options, NULL);
}
- 定义位置:
kernel/exit.c
- 功能:
waitpid
是wait4
的简化版本,主要用于兼容性。
waitid
系统调用:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
infop, int, options, struct rusage __user *, ru)
{
struct rusage r;
struct waitid_info info = {.status = 0};
long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
int signo = 0;
if (err > 0) {
signo = SIGCHLD;
err = 0;
if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
return -EFAULT;
}
if (!infop)
return err;
if (!user_write_access_begin(infop, sizeof(*infop)))
return -EFAULT;
unsafe_put_user(signo, &infop->si_signo, Efault);
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user(info.cause, &infop->si_code, Efault);
unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault);
unsafe_put_user(info.status, &infop->si_status, Efault);
user_write_access_end();
return err;
Efault:
user_write_access_end();
return -EFAULT;
}
- 定义位置:
kernel/exit.c
- 功能:提供更灵活的等待机制,可以指定等待的进程类型。
通过代码中可以看到所有的 wait
系统调用是通过 kernel_wait4
、kernel_waitid
完成核心释放任务,而这两个函数的处理流程大致相同,都是会先处理参数,通过参数准备 struct wait_opts
结构体作为参数再去调用 do_wait
函数来完成实际释放操作。
9.2. 核心数据结构 struct wait_opts
struct wait_opts
用于封装 wait
系统调用的参数和状态。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.h
struct wait_opts {
enum pid_type wo_type; // 等待的进程类型 (PID, PGID, 等)
int wo_flags; // 等待选项 (如 WNOHANG, WEXITED)
struct pid *wo_pid; // 等待的目标进程
struct waitid_info *wo_info; // 用于存储返回的进程信息
int wo_stat; // 子进程的退出状态
struct rusage *wo_rusage; // 子进程的资源使用信息
wait_queue_entry_t child_wait; // 等待队列条目
int notask_error; // 错误状态
};
struct waitid_info
用于存储子进程的状态信息。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.h
struct waitid_info {
pid_t pid; // 子进程的 PID
uid_t uid; // 子进程的 UID
int status; // 子进程的退出状态
int cause; // 状态变化的原因 (如 CLD_EXITED, CLD_KILLED)
};
9.3. do_wait()
do_wait
是等待逻辑的核心函数,其参数就是上面所列的 struct wait_opts
,负责遍历子进程并检查其状态,处理父进程等待子进程状态变化的逻辑。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static long do_wait(struct wait_opts *wo)
{
int retval;
// 调用 trace_sched_process_wait 跟踪调度事件,记录当前进程正在等待的子进程的 PID。
trace_sched_process_wait(wo->wo_pid);
// 初始化等待队列条目 wo->child_wait,并将其添加到当前进程的 wait_chldexit 等待队列中。
// child_wait_callback 是一个回调函数,用于在子进程状态变化时唤醒父进程。
init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
wo->child_wait.private = current;
add_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
do {
set_current_state(TASK_INTERRUPTIBLE);
retval = __do_wait(wo);
if (retval != -ERESTARTSYS)
break;
if (signal_pending(current))
break;
schedule();
} while (1);
__set_current_state(TASK_RUNNING);
remove_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
return retval;
}
9.3.1 初始化等待队列
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_wait()
init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
wo->child_wait.private = current;
add_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
- 初始化等待队列条目
wo->child_wait
,并将其添加到当前进程的wait_chldexit
等待队列中。 child_wait_callback
是一个回调函数,用于在子进程状态变化时唤醒父进程。
9.3.2 进入等待循环
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_wait()
do {
set_current_state(TASK_INTERRUPTIBLE);
retval = __do_wait(wo);
if (retval != -ERESTARTSYS)
break;
if (signal_pending(current))
break;
schedule();
} while (1);
-
set_current_state(TASK_INTERRUPTIBLE)
:- 将当前进程的状态设置为可中断的睡眠状态 (
TASK_INTERRUPTIBLE
)。 - 这样,当前进程可以被信号唤醒。
- 将当前进程的状态设置为可中断的睡眠状态 (
-
调用
__do_wait
:__do_wait
是do_wait
的核心逻辑,负责检查子进程的状态并返回结果。- 如果
__do_wait
返回-ERESTARTSYS
,表示需要重新执行等待操作。
-
检查信号:
- 如果当前进程收到信号 (
signal_pending(current)
返回true
),则退出等待循环。
- 如果当前进程收到信号 (
-
调用
schedule
:- 如果没有信号,调用
schedule
将当前进程调度出去,等待子进程状态变化或被唤醒。
- 如果没有信号,调用
9.3.3. 恢复运行状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_wait()
__set_current_state(TASK_RUNNING);
remove_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
- 将当前进程的状态恢复为运行状态 (
TASK_RUNNING
)。 - 从
wait_chldexit
等待队列中移除当前进程。
9.3.4. 返回结果
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: do_wait()
return retval;
- 返回
__do_wait
的结果,表示子进程的状态或错误码。
如果当前进程在等待过程中收到信号,do_wait
会立即退出等待循环。这使得 wait
系统调用可以被信号中断,从而提高了系统的响应性。do_wait
使用等待队列 (wait_chldexit
) 实现阻塞等待。当子进程状态发生变化时,内核会唤醒等待队列中的父进程。
9.4. __do_wait()
__do_wait
是 do_wait
的核心逻辑,其参数仍然是 struct wait_opts
,负责遍历子进程列表,检查其状态是否满足等待条件。如果找到符合条件的子进程,__do_wait
会返回其状态或相关信息。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
long __do_wait(struct wait_opts *wo)
{
long retval;
/*
* If there is nothing that can match our criteria, just get out.
* We will clear ->notask_error to zero if we see any child that
* might later match our criteria, even if we are not able to reap
* it yet.
*/
// 如果没有符合条件的子进程,返回 -ECHILD 错误,表示当前进程没有子进程。
wo->notask_error = -ECHILD;
// 如果指定的等待目标不存在(例如目标 PID 无效),直接跳转到 notask 标签,返回错误。
if ((wo->wo_type < PIDTYPE_MAX) &&
(!wo->wo_pid || !pid_has_task(wo->wo_pid, wo->wo_type)))
goto notask;
// 使用 tasklist_lock 读锁保护任务列表,防止在遍历子进程时发生并发修改。
read_lock(&tasklist_lock);
if (wo->wo_type == PIDTYPE_PID) {
retval = do_wait_pid(wo);
if (retval)
return retval;
} else {
struct task_struct *tsk = current;
do {
retval = do_wait_thread(wo, tsk);
if (retval)
return retval;
retval = ptrace_do_wait(wo, tsk);
if (retval)
return retval;
if (wo->wo_flags & __WNOTHREAD)
break;
} while_each_thread(current, tsk);
}
// 遍历完成后,释放 tasklist_lock 读锁。
read_unlock(&tasklist_lock);
notask:
retval = wo->notask_error;
if (!retval && !(wo->wo_flags & WNOHANG))
return -ERESTARTSYS;
return retval;
}
9.4.1. 处理不同的等待类型
- 等待特定 PID 的子进程:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: __do_wait()
if (wo->wo_type == PIDTYPE_PID) {
retval = do_wait_pid(wo);
if (retval)
return retval;
}
-
调用
do_wait_pid
,直接处理指定PID
的子进程。 -
如果找到符合条件的子进程,返回其状态。
-
等待所有子进程:
// Linux Kernel 6.15.0-rc2 // PATH: kernel/exit.c // Function: __do_wait() struct task_struct *tsk = current; do { retval = do_wait_thread(wo, tsk); if (retval) return retval; retval = ptrace_do_wait(wo, tsk); if (retval) return retval; if (wo->wo_flags & __WNOTHREAD) break; } while_each_thread(current, tsk);
- 遍历当前线程组的所有线程,依次调用:
do_wait_thread
: 处理普通子进程。ptrace_do_wait
: 处理被ptrace
跟踪的子进程。
- 如果找到符合条件的子进程,返回其状态。
- 如果设置了
__WNOTHREAD
标志,则只处理当前线程,不遍历线程组中的其他线程。
- 遍历当前线程组的所有线程,依次调用:
9.4.2. 处理无匹配子进程的情况
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: __do_wait()
notask:
retval = wo->notask_error;
if (!retval && !(wo->wo_flags & WNOHANG))
return -ERESTARTSYS;
- 如果没有找到符合条件的子进程:
- 如果设置了
WNOHANG
标志,立即返回0
,表示非阻塞模式下没有子进程状态变化。 - 否则返回
-ERESTARTSYS
,表示需要重新执行等待操作。
- 如果设置了
9.4.3. 返回结果
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: __do_wait()
return retval;
- 返回子进程的状态或错误码。
其实可以看到真正的释放工作也不在当前函数中,在 __do_wait
中有三处用来处理不同类型进程,通过三个函数的实现其实可以看到,最终都会调用 wait_consider_task
完成真正的处理工作。
do_wait_pid
用于处理特定PID
子进程;do_wait_thread
用于处理普通子进程;ptrace_do_wait
用于处理被ptrace
跟踪的子进程。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static int do_wait_pid(struct wait_opts *wo)
{
bool ptrace;
struct task_struct *target;
int retval;
ptrace = false;
target = pid_task(wo->wo_pid, PIDTYPE_TGID);
if (target && is_effectively_child(wo, ptrace, target)) {
retval = wait_consider_task(wo, ptrace, target);
if (retval)
return retval;
}
ptrace = true;
target = pid_task(wo->wo_pid, PIDTYPE_PID);
if (target && target->ptrace &&
is_effectively_child(wo, ptrace, target)) {
retval = wait_consider_task(wo, ptrace, target);
if (retval)
return retval;
}
return 0;
}
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static int do_wait_thread(struct wait_opts *wo, struct task_struct *tsk)
{
struct task_struct *p;
list_for_each_entry(p, &tsk->children, sibling) {
int ret = wait_consider_task(wo, 0, p);
if (ret)
return ret;
}
return 0;
}
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static int ptrace_do_wait(struct wait_opts *wo, struct task_struct *tsk)
{
struct task_struct *p;
list_for_each_entry(p, &tsk->ptraced, ptrace_entry) {
int ret = wait_consider_task(wo, 1, p);
if (ret)
return ret;
}
return 0;
}
9.5. wait_consider_task()
wait_consider_task
用于检查一个特定的子进程是否满足父进程的等待条件。它会根据子进程的状态和父进程的等待选项,决定是否返回子进程的状态。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static int wait_consider_task(struct wait_opts *wo, int ptrace,
struct task_struct *p)
{
/*
* We can race with wait_task_zombie() from another thread.
* Ensure that EXIT_ZOMBIE -> EXIT_DEAD/EXIT_TRACE transition
* can't confuse the checks below.
*/
int exit_state = READ_ONCE(p->exit_state);
int ret;
if (unlikely(exit_state == EXIT_DEAD))
return 0;
ret = eligible_child(wo, ptrace, p);
if (!ret)
return ret;
if (unlikely(exit_state == EXIT_TRACE)) {
/*
* ptrace == 0 means we are the natural parent. In this case
* we should clear notask_error, debugger will notify us.
*/
if (likely(!ptrace))
wo->notask_error = 0;
return 0;
}
if (likely(!ptrace) && unlikely(p->ptrace)) {
/*
* If it is traced by its real parent's group, just pretend
* the caller is ptrace_do_wait() and reap this child if it
* is zombie.
*
* This also hides group stop state from real parent; otherwise
* a single stop can be reported twice as group and ptrace stop.
* If a ptracer wants to distinguish these two events for its
* own children it should create a separate process which takes
* the role of real parent.
*/
if (!ptrace_reparented(p))
ptrace = 1;
}
/* slay zombie? */
if (exit_state == EXIT_ZOMBIE) {
/* we don't reap group leaders with subthreads */
if (!delay_group_leader(p)) {
/*
* A zombie ptracee is only visible to its ptracer.
* Notification and reaping will be cascaded to the
* real parent when the ptracer detaches.
*/
if (unlikely(ptrace) || likely(!p->ptrace))
return wait_task_zombie(wo, p);
}
/*
* Allow access to stopped/continued state via zombie by
* falling through. Clearing of notask_error is complex.
*
* When !@ptrace:
*
* If WEXITED is set, notask_error should naturally be
* cleared. If not, subset of WSTOPPED|WCONTINUED is set,
* so, if there are live subthreads, there are events to
* wait for. If all subthreads are dead, it's still safe
* to clear - this function will be called again in finite
* amount time once all the subthreads are released and
* will then return without clearing.
*
* When @ptrace:
*
* Stopped state is per-task and thus can't change once the
* target task dies. Only continued and exited can happen.
* Clear notask_error if WCONTINUED | WEXITED.
*/
if (likely(!ptrace) || (wo->wo_flags & (WCONTINUED | WEXITED)))
wo->notask_error = 0;
} else {
/*
* @p is alive and it's gonna stop, continue or exit, so
* there always is something to wait for.
*/
wo->notask_error = 0;
}
/*
* Wait for stopped. Depending on @ptrace, different stopped state
* is used and the two don't interact with each other.
*/
ret = wait_task_stopped(wo, ptrace, p);
if (ret)
return ret;
/*
* Wait for continued. There's only one continued state and the
* ptracer can consume it which can confuse the real parent. Don't
* use WCONTINUED from ptracer. You don't need or want it.
*/
return wait_task_continued(wo, p);
}
9.5.1. 函数参数
-
struct wait_opts *wo
:- 包含等待操作的参数和状态信息
-
int ptrace
:- 指示当前是否在处理被
ptrace
跟踪的子进程。 1
表示是被跟踪的子进程,0
表示普通子进程。
- 指示当前是否在处理被
-
struct task_struct *p
:- 当前正在检查的子进程。
9.5.2. 检查子进程的退出状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
int exit_state = READ_ONCE(p->exit_state);
if (unlikely(exit_state == EXIT_DEAD))
return 0;
- 使用
READ_ONCE
读取子进程的退出状态,防止并发修改。 - 如果子进程的退出状态已经处于
EXIT_DEAD
状态,直接返回0
,表示该子进程不可用。(该状态的进程可以被直接释放)
9.5.3. 检查子进程是否符合等待条件
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
ret = eligible_child(wo, ptrace, p);
if (!ret)
return ret;
- 调用
eligible_child
检查子进程是否符合父进程的等待条件。 - 如果子进程不符合条件,直接返回
0
。
9.5.4. 处理 EXIT_TRACE
状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
if (unlikely(exit_state == EXIT_TRACE)) {
if (likely(!ptrace))
wo->notask_error = 0;
return 0;
}
- 如果子进程处于
EXIT_TRACE
状态,表示它正在被ptrace
跟踪。 - 如果当前不是
ptrace
的上下文,则清除notask_error
并返回0
。
9.5.5. 处理被 ptrace
跟踪的子进程
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
if (likely(!ptrace) && unlikely(p->ptrace)) {
if (!ptrace_reparented(p))
ptrace = 1;
}
- 如果子进程被
ptrace
跟踪,并且当前不是ptrace
的上下文,则将ptrace
标志设置为1
。
9.5.6. 处理 EXIT_ZOMBIE
状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
if (exit_state == EXIT_ZOMBIE) {
if (!delay_group_leader(p)) {
if (unlikely(ptrace) || likely(!p->ptrace))
return wait_task_zombie(wo, p);
}
if (likely(!ptrace) || (wo->wo_flags & (WCONTINUED | WEXITED)))
wo->notask_error = 0;
}
- 如果子进程的退出状态处于
EXIT_ZOMBIE
状态:- 如果子进程是组领导者且有子线程,则不会立即回收。
- 否则调用
wait_task_zombie
处理僵尸进程。(会将进程退出状态改变为EXIT_DEAD
,并彻底释放)
- 如果满足条件,清除
notask_error
。
9.5.7. 处理其他状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
wo->notask_error = 0;
- 如果子进程处于其他状态(如运行、停止等),清除
notask_error
,表示仍有子进程可供等待。
9.5.8. 等待停止状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
ret = wait_task_stopped(wo, ptrace, p);
if (ret)
return ret;
- 调用
wait_task_stopped
检查子进程是否处于停止状态。 - 如果满足条件,返回子进程的状态。
9.5.9. 等待继续状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: wait_consider_task()
return wait_task_continued(wo, p);
- 调用
wait_task_continued
检查子进程是否处于继续状态。 - 返回子进程的状态。
wait_consider_task
会根据子进程的状态(如 EXIT_ZOMBIE
, EXIT_TRACE
, 停止、继续等)调用不同的处理函数:
wait_task_zombie
: 处理僵尸进程。wait_task_stopped
: 处理停止状态。wait_task_continued
: 处理继续状态。
而通过之前的分析我们也知道了通过调用 do_exit
函数的进程只是被清理了大部分的内核资源,并不会真正的消亡,而是将其进程退出状态(exit_state
)设置为 EXIT_ZOMBIE
状态,而进程状态(__state
)设置为 TASK_DEAD
存在(虽然不会被调度,但还是会占用内存资源)。那么执行完 do_exit
到这里的处理大部分情况都会进入 wait_task_zombie
去处理僵尸进程。
9.6. wait_task_zombie()
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static int wait_task_zombie(struct wait_opts *wo, struct task_struct *p)
{
int state, status;
pid_t pid = task_pid_vnr(p);
uid_t uid = from_kuid_munged(current_user_ns(), task_uid(p));
struct waitid_info *infop;
// 如果父进程没有设置 WEXITED 标志,则直接返回,表示当前子进程不符合等待条件。
if (!likely(wo->wo_flags & WEXITED))
return 0;
// 如果设置了 WNOWAIT 标志,父进程只获取子进程的状态信息,但不会回收子进程。
if (unlikely(wo->wo_flags & WNOWAIT)) {
status = (p->signal->flags & SIGNAL_GROUP_EXIT)
? p->signal->group_exit_code : p->exit_code;
get_task_struct(p);
read_unlock(&tasklist_lock);
sched_annotate_sleep();
if (wo->wo_rusage)
getrusage(p, RUSAGE_BOTH, wo->wo_rusage);
put_task_struct(p);
// 子进程仍然保持 EXIT_ZOMBIE 状态。退出函数
goto out_info;
}
/*
* Move the task's state to DEAD/TRACE, only one thread can do this.
*/
// 使用 cmpxchg 原子操作将子进程的状态从 EXIT_ZOMBIE 转换为 EXIT_DEAD 或 EXIT_TRACE。
state = (ptrace_reparented(p) && thread_group_leader(p)) ?
EXIT_TRACE : EXIT_DEAD;
// 如果状态已经被其他线程修改,则直接返回。
if (cmpxchg(&p->exit_state, EXIT_ZOMBIE, state) != EXIT_ZOMBIE)
return 0;
/*
* We own this thread, nobody else can reap it.
*/
// 释放 tasklist_lock 锁,允许其他线程访问任务列表。
read_unlock(&tasklist_lock);
sched_annotate_sleep();
/*
* Check thread_group_leader() to exclude the traced sub-threads.
*/
// 如果子进程是线程组的领导者,累积线程组的资源使用统计信息(如 CPU 时间、内存页错误等)。
if (state == EXIT_DEAD && thread_group_leader(p)) {
struct signal_struct *sig = p->signal;
struct signal_struct *psig = current->signal;
unsigned long maxrss;
u64 tgutime, tgstime;
/*
* The resource counters for the group leader are in its
* own task_struct. Those for dead threads in the group
* are in its signal_struct, as are those for the child
* processes it has previously reaped. All these
* accumulate in the parent's signal_struct c* fields.
*
* We don't bother to take a lock here to protect these
* p->signal fields because the whole thread group is dead
* and nobody can change them.
*
* psig->stats_lock also protects us from our sub-threads
* which can reap other children at the same time.
*
* We use thread_group_cputime_adjusted() to get times for
* the thread group, which consolidates times for all threads
* in the group including the group leader.
*/
thread_group_cputime_adjusted(p, &tgutime, &tgstime);
write_seqlock_irq(&psig->stats_lock);
psig->cutime += tgutime + sig->cutime;
psig->cstime += tgstime + sig->cstime;
psig->cgtime += task_gtime(p) + sig->gtime + sig->cgtime;
psig->cmin_flt +=
p->min_flt + sig->min_flt + sig->cmin_flt;
psig->cmaj_flt +=
p->maj_flt + sig->maj_flt + sig->cmaj_flt;
psig->cnvcsw +=
p->nvcsw + sig->nvcsw + sig->cnvcsw;
psig->cnivcsw +=
p->nivcsw + sig->nivcsw + sig->cnivcsw;
psig->cinblock +=
task_io_get_inblock(p) +
sig->inblock + sig->cinblock;
psig->coublock +=
task_io_get_oublock(p) +
sig->oublock + sig->coublock;
maxrss = max(sig->maxrss, sig->cmaxrss);
if (psig->cmaxrss < maxrss)
psig->cmaxrss = maxrss;
task_io_accounting_add(&psig->ioac, &p->ioac);
task_io_accounting_add(&psig->ioac, &sig->ioac);
write_sequnlock_irq(&psig->stats_lock);
}
if (wo->wo_rusage)
getrusage(p, RUSAGE_BOTH, wo->wo_rusage);
status = (p->signal->flags & SIGNAL_GROUP_EXIT)
? p->signal->group_exit_code : p->exit_code;
wo->wo_stat = status;
// 如果子进程处于 EXIT_TRACE 状态,表示它被 ptrace 跟踪。
if (state == EXIT_TRACE) {
write_lock_irq(&tasklist_lock);
/* We dropped tasklist, ptracer could die and untrace */
ptrace_unlink(p);
/* If parent wants a zombie, don't release it now */
state = EXIT_ZOMBIE;
// 如果父进程需要通知,则可能将状态设置为 EXIT_DEAD。
if (do_notify_parent(p, p->exit_signal))
state = EXIT_DEAD;
p->exit_state = state;
write_unlock_irq(&tasklist_lock);
}
// 如果子进程的状态是 EXIT_DEAD,调用 release_task 释放子进程的资源。
if (state == EXIT_DEAD)
release_task(p);
out_info:
// 填充 waitid_info 结构,返回子进程的退出状态、PID 和 UID。并返回子进程 pid
infop = wo->wo_info;
if (infop) {
if ((status & 0x7f) == 0) {
infop->cause = CLD_EXITED;
infop->status = status >> 8;
} else {
infop->cause = (status & 0x80) ? CLD_DUMPED : CLD_KILLED;
infop->status = status & 0x7f;
}
infop->pid = pid;
infop->uid = uid;
}
return pid;
}
9.6.1. 函数参数
-
struct wait_opts wo
:- 包含等待操作的参数和状态信息。
- 包括等待选项(如
WEXITED
,WNOWAIT
)、资源使用信息指针等。
-
struct task_struct *p
:- 当前正在处理的子进程。
9.6.2. 主要工作流程
这里简要分析 wait_task_zombie
函数功能,该函数负责处理处于 EXIT_ZOMBIE
状态的子进程。它负责将子进程的状态从 EXIT_ZOMBIE
转换为 EXIT_DEAD
。累积线程组的资源使用统计信息。根据父进程的等待选项返回子进程的状态信息。当子进程的状态被设置为 EXIT_DEAD
时,最后调用 release_task
释放子进程的资源。这里我们主要关注最后调用的 release_task
函数。
9.7. release_task()
还记得吗?在之前子进程执行 do_exit
函数中的 exit_notify
函数中最后也会调用 release_task
函数,而那时笔者备注将在后文说明,那么现在是时候梳理该函数了。该函数用于清理进程的所有资源并将其从内核中移除。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
void release_task(struct task_struct *p)
{
struct release_task_post post;
struct task_struct *leader;
struct pid *thread_pid;
int zap_leader;
repeat:
memset(&post, 0, sizeof(post));
/* don't need to get the RCU readlock here - the process is dead and
* can't be modifying its own credentials. But shut RCU-lockdep up */
// 更新用户计数
rcu_read_lock();
dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
rcu_read_unlock();
// 释放与 PID 文件系统相关的资源
pidfs_exit(p);
// 释放 cgroup 相关资源
cgroup_release(p);
// 获取线程的 PID
thread_pid = get_pid(p->thread_pid);
// 加锁以操作任务列表
write_lock_irq(&tasklist_lock);
ptrace_release_task(p);
__exit_signal(&post, p);
/*
* If we are the last non-leader member of the thread
* group, and the leader is zombie, then notify the
* group leader's parent process. (if it wants notification.)
*/
// 检查是否需要通知线程组 leader 的父进程
zap_leader = 0;
leader = p->group_leader;
if (leader != p && thread_group_empty(leader)
&& leader->exit_state == EXIT_ZOMBIE) {
/* for pidfs_exit() and do_notify_parent() */
if (leader->signal->flags & SIGNAL_GROUP_EXIT)
leader->exit_code = leader->signal->group_exit_code;
/*
* If we were the last child thread and the leader has
* exited already, and the leader's parent ignores SIGCHLD,
* then we are the one who should release the leader.
*/
zap_leader = do_notify_parent(leader, leader->exit_signal);
if (zap_leader)
leader->exit_state = EXIT_DEAD;
}
write_unlock_irq(&tasklist_lock);
// 清理 PID 相关资源
proc_flush_pid(thread_pid);
put_pid(thread_pid);
add_device_randomness(&p->se.sum_exec_runtime,
sizeof(p->se.sum_exec_runtime));
// 释放 PID 相关资源
free_pids(post.pids);
// 释放线程相关资源
release_thread(p);
/*
* This task was already removed from the process/thread/pid lists
* and lock_task_sighand(p) can't succeed. Nobody else can touch
* ->pending or, if group dead, signal->shared_pending. We can call
* flush_sigqueue() lockless.
*/
// 清理信号队列
flush_sigqueue(&p->pending);
if (thread_group_leader(p))
flush_sigqueue(&p->signal->shared_pending);
put_task_struct_rcu_user(p);
// 如果需要,重复处理线程组 leader
p = leader;
if (unlikely(zap_leader))
goto repeat;
}
9.7.1. 函数参数
struct task_struct *p
:- 指向需要释放的进程的任务结构(进程描述符)。
9.7.2. 初始化清理状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
memset(&post, 0, sizeof(post));
- 初始化
release_task_post
结构,用于存储需要延迟清理的资源(如 PID)。
9.7.3. 减少进程计数
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
rcu_read_lock();
dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
rcu_read_unlock();
- 减少当前用户的进程计数(
RLIMIT_NPROC
限制)。 - 使用 RCU 机制确保并发安全。
9.7.4. 释放与进程相关的子系统资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
pidfs_exit(p);
cgroup_release(p);
pidfs_exit
:- 从
/proc
文件系统中移除与进程相关的条目。
- 从
cgroup_release
:- 从控制组(
cgroup
)中移除进程。
- 从控制组(
9.7.5. 获取线程 PID
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
thread_pid = get_pid(p->thread_pid);
- 获取进程的线程
PID
,以便稍后释放。
9.7.6. 加锁并清理信号和调试信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
write_lock_irq(&tasklist_lock);
ptrace_release_task(p);
__exit_signal(&post, p);
ptrace_release_task
:- 释放与
ptrace
调试相关的资源。
- 释放与
__exit_signal
:- 清理信号相关的资源,包括释放信号队列、更新统计信息等。该函数会调用
__unhash_process
函数完成将待释放进程的pid
结构从各种哈希表和链表中移除(包括:PID
哈希表、TGID
(线程组ID
)、PGID
(进程组ID
)、SID
(会话ID
)、RCU
方式从全局进程链表(task_struct.tasks
,即进程号顺序链表)中移除该进程和从线程组链表(thread_node
)中移除该线程、从父进程的子进程链表中移除该进程,最后使当前CPU
的进程计数减一)
- 清理信号相关的资源,包括释放信号队列、更新统计信息等。该函数会调用
9.7.7. 处理线程组领导者
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
zap_leader = 0;
leader = p->group_leader;
if (leader != p && thread_group_empty(leader)
&& leader->exit_state == EXIT_ZOMBIE) {
if (leader->signal->flags & SIGNAL_GROUP_EXIT)
leader->exit_code = leader->signal->group_exit_code;
zap_leader = do_notify_parent(leader, leader->exit_signal);
if (zap_leader)
leader->exit_state = EXIT_DEAD;
}
- 如果当前进程是线程组的最后一个非领导线程,并且线程组领导者处于
EXIT_ZOMBIE
状态:- 通知线程组领导者的父进程。
- 如果父进程忽略了
SIGCHLD
信号,则将线程组领导者的状态设置为EXIT_DEAD
。
9.7.8. 释放 PID 和其他资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
write_unlock_irq(&tasklist_lock);
proc_flush_pid(thread_pid);
put_pid(thread_pid);
add_device_randomness(&p->se.sum_exec_runtime,
sizeof(p->se.sum_exec_runtime));
free_pids(post.pids);
release_thread(p);
proc_flush_pid
:- 从
/proc
文件系统中移除与PID
相关的条目。
- 从
put_pid
:- 释放
PID
的引用计数。
- 释放
release_thread
:- 释放与线程相关的架构特定资源。
9.7.9. 清理信号队列
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
flush_sigqueue(&p->pending);
if (thread_group_leader(p))
flush_sigqueue(&p->signal->shared_pending);
- 清理进程的信号队列。
- 如果当前进程是线程组的领导者,还需要清理共享信号队列。
9.7.10. 释放任务结构
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
put_task_struct_rcu_user(p);
- 使用
RCU
延迟机制释放任务结构,确保并发安全。(在这里将最终释放进程描述符)
9.7.11. 处理线程组领导者的释放
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
// Function: release_task()
p = leader;
if (unlikely(zap_leader))
goto repeat;
- 如果需要释放线程组领导者,则重复上述流程。
如果当前进程是线程组的一部分,release_task
会确保线程组领导者的父进程被正确通知,并在必要时释放线程组领导者。release_task
会从 /proc
文件系统和 cgroup
中移除进程,确保系统状态的一致性。release_task
会清理所有与信号和 ptrace
调试相关的资源,确保进程退出后不会留下任何残留。使用 put_task_struct_rcu_user
延迟释放任务结构,确保在并发环境下的安全性。
9.8. put_task_struct_rcu_user()
记得吗?在 do_exit
函数最后的调用里也调用了该函数。在 release_task
函数中,进程描述符的彻底移除发生在调用 put_task_struct_rcu_user
时。这一步通过 RCU
延迟机制释放进程描述符,确保并发访问的安全性。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static void delayed_put_task_struct(struct rcu_head *rhp)
{
struct task_struct *tsk = container_of(rhp, struct task_struct, rcu);
kprobe_flush_task(tsk);
rethook_flush_task(tsk);
perf_event_delayed_put(tsk);
trace_sched_process_free(tsk);
put_task_struct(tsk);
}
void put_task_struct_rcu_user(struct task_struct *task)
{
if (refcount_dec_and_test(&task->rcu_users))
call_rcu(&task->rcu, delayed_put_task_struct);
}
9.8.1. put_task_struct_rcu_user 的作用
put_task_struct_rcu_user
函数会减少进程描述符的引用计数(rcu_users
)。- 如果引用计数归零(
rcu_users
==0
),则调用call_rcu
延迟释放进程描述符。
- 延迟释放机制
call_rcu
确保在所有可能访问该进程描述符的读操作完成后,才会真正释放内存。- 延迟释放的回调函数是
delayed_put_task_struct
。
9.8.2. delayed_put_task_struct 的作用
- 这是最终释放进程描述符的函数。
- 它会清理与进程相关的剩余资源,并调用
put_task_struct
释放进程描述符的内存。
put_task_struct
的作用:put_task_struct
是真正释放进程描述符内存的函数。- 当引用计数归零时,释放
task_struct
的内存。
9.9. put_task_struct()
9.9.1. 函数定义
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched/task.h
static inline void put_task_struct(struct task_struct *t)
{
if (!refcount_dec_and_test(&t->usage))
return;
/*
* In !RT, it is always safe to call __put_task_struct().
* Under RT, we can only call it in preemptible context.
*/
if (!IS_ENABLED(CONFIG_PREEMPT_RT) || preemptible()) {
static DEFINE_WAIT_OVERRIDE_MAP(put_task_map, LD_WAIT_SLEEP);
lock_map_acquire_try(&put_task_map);
__put_task_struct(t);
lock_map_release(&put_task_map);
return;
}
/*
* Under PREEMPT_RT, we can't call __put_task_struct
* in atomic context because it will indirectly
* acquire sleeping locks.
*
* call_rcu() will schedule delayed_put_task_struct_rcu()
* to be called in process context.
*/
call_rcu(&t->rcu, __put_task_struct_rcu_cb);
}
9.9.2. 函数参数
struct task_struct *t
:- 指向需要减少引用计数的进程描述符。
9.9.3. 减少引用计数
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched/task.h
// Function: put_task_struct()
if (!refcount_dec_and_test(&t->usage))
return;
- 使用
refcount_dec_and_test
减少task_struct
的引用计数。 - 如果引用计数未归零,则直接返回,表示该进程描述符仍在被其他地方引用。
9.9.4. 检查是否可以直接释放
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched/task.h
// Function: put_task_struct()
if (!IS_ENABLED(CONFIG_PREEMPT_RT) || preemptible()) {
static DEFINE_WAIT_OVERRIDE_MAP(put_task_map, LD_WAIT_SLEEP);
lock_map_acquire_try(&put_task_map);
__put_task_struct(t);
lock_map_release(&put_task_map);
return;
}
- 如果内核未启用
PREEMPT_RT
(实时抢占)功能,或者当前处于可抢占的上下文中,则可以直接调用__put_task_struct
释放进程描述符。 __put_task_struct
:- 这是实际释放
task_struct
内存的函数。
- 这是实际释放
由于在子进程执行 do_exit
函数中已经关闭了内核抢占,并且内核的配置宏 CONFIG_PREEMPT_RT
未开启,因此都会进入该分支执行。
9.9.5. 延迟释放(针对 PREEMPT_RT
)
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched/task.h
// Function: put_task_struct()
call_rcu(&t->rcu, __put_task_struct_rcu_cb);
- 如果启用了
PREEMPT_RT
并且当前处于不可抢占的上下文中,则不能直接调用__put_task_struct
,因为它可能会获取睡眠锁。 - 使用
call_rcu
延迟释放进程描述符:- 将
__put_task_struct_rcu_cb
注册为回调函数。 - 当
RCU
读操作完成后,__put_task_struct_rcu_cb
会被调用。
- 将
9.9.5. 延迟释放(针对 PREEMPT_RT
)
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/sched/task.h
// Function: put_task_struct()
call_rcu(&t->rcu, __put_task_struct_rcu_cb);
- 如果启用了
PREEMPT_RT
并且当前处于不可抢占的上下文中,则不能直接调用__put_task_struct
,因为它可能会获取睡眠锁。 - 使用
call_rcu
延迟释放进程描述符:- 将
__put_task_struct_rcu_cb
注册为回调函数。 - 当
RCU
读操作完成后,__put_task_struct_rcu_cb
会被调用。
- 将
9.9.6. 延迟释放的回调函数 __put_task_struct_rcu_cb
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
void __put_task_struct_rcu_cb(struct rcu_head *rhp)
{
struct task_struct *tsk = container_of(rhp, struct task_struct, rcu);
__put_task_struct(tsk);
}
- 这是
call_rcu
的回调函数。 - 它会调用
__put_task_struct
实际释放task_struct
的内存。
put_task_struct
主要作用是减少进程描述符的引用计数,并在引用计数归零时调用 __put_task_struct
释放进程描述符的内存。
9.10. __put_task_struct()
__put_task_struct
在进程的引用计数归零后被调用,负责清理与进程相关的所有资源并最终释放 task_struct
的内存。
9.10.1. 函数定义
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
void __put_task_struct(struct task_struct *tsk)
{
WARN_ON(!tsk->exit_state);
WARN_ON(refcount_read(&tsk->usage));
WARN_ON(tsk == current);
sched_ext_free(tsk);
io_uring_free(tsk);
cgroup_free(tsk);
task_numa_free(tsk, true);
security_task_free(tsk);
exit_creds(tsk);
delayacct_tsk_free(tsk);
put_signal_struct(tsk->signal);
sched_core_free(tsk);
free_task(tsk);
}
EXPORT_SYMBOL_GPL(__put_task_struct);
9.10.2. 函数参数
struct task_struct *tsk
:- 指向需要释放的进程描述符。
9.10.3. 检查进程状态
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
WARN_ON(!tsk->exit_state);
WARN_ON(refcount_read(&tsk->usage));
WARN_ON(tsk == current);
WARN_ON
:- 检查以下条件是否满足,如果不满足则打印警告:
- 进程是否已经处于退出状态(
exit_state
)。 - 进程的引用计数是否已经归零(
usage
)。 - 当前进程是否试图释放自身(
tsk == current
)。
- 进程是否已经处于退出状态(
- 检查以下条件是否满足,如果不满足则打印警告:
9.10.4. 释放调度相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
sched_ext_free(tsk);
sched_core_free(tsk);
sched_ext_free
:- 释放与调度扩展(如实时调度)相关的资源。
sched_core_free
:- 释放与调度核心相关的资源。
9.10.5. 释放 I/O 相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
io_uring_free(tsk);
io_uring_free
:- 释放与
I/O
子系统(如io_uring
)相关的资源。
- 释放与
9.10.6. 释放控制组(cgroup)资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
cgroup_free(tsk);
cgroup_free
:- 释放进程在控制组(cgroup)中的资源。
9.10.7. 释放 NUMA 相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
task_numa_free(tsk, true);
task_numa_free
:- 释放与
NUMA
(非一致性内存访问)相关的资源。
- 释放与
9.10.8. 释放安全模块相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
security_task_free(tsk);
security_task_free
:- 释放与安全模块(如
SELinux
、AppArmor
)相关的资源。
- 释放与安全模块(如
9.10.9. 释放进程凭据
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
exit_creds(tsk);
exit_creds
:- 释放进程的凭据(如用户
ID
、组ID
、权限等)。
- 释放进程的凭据(如用户
9.10.10. 释放延迟统计资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
delayacct_tsk_free(tsk);
delayacct_tsk_free
:- 释放与延迟统计(如 I/O 延迟、调度延迟)相关的资源。
9.10.11. 释放信号相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
put_signal_struct(tsk->signal);
put_signal_struct
:- 减少信号结构的引用计数,如果引用计数归零,则释放信号结构。
9.10.12. 释放任务描述符
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: __put_task_struct()
free_task(tsk);
free_task
:- 最终释放
task_struct
的内存。(真正真正释放task_struct
内存的地方)
- 最终释放
9.11. 最终释放 free_task()
free_task
在进程的生命周期结束时被调用,负责清理与进程相关的所有资源。
9.11.1. 函数定义
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
void free_task(struct task_struct *tsk)
{
#ifdef CONFIG_SECCOMP
WARN_ON_ONCE(tsk->seccomp.filter);
#endif
release_user_cpus_ptr(tsk);
scs_release(tsk);
#ifndef CONFIG_THREAD_INFO_IN_TASK
/*
* The task is finally done with both the stack and thread_info,
* so free both.
*/
release_task_stack(tsk);
#else
/*
* If the task had a separate stack allocation, it should be gone
* by now.
*/
WARN_ON_ONCE(refcount_read(&tsk->stack_refcount) != 0);
#endif
rt_mutex_debug_task_free(tsk);
ftrace_graph_exit_task(tsk);
arch_release_task_struct(tsk);
if (tsk->flags & PF_KTHREAD)
free_kthread_struct(tsk);
bpf_task_storage_free(tsk);
free_task_struct(tsk);
}
EXPORT_SYMBOL(free_task);
9.11.2. 函数参数
struct task_struct *tsk
:- 指向需要释放的进程描述符。
9.11.3. 检查 Seccomp 过滤器
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
#ifdef CONFIG_SECCOMP
WARN_ON_ONCE(tsk->seccomp.filter);
#endif
- 如果启用了
CONFIG_SECCOMP
,检查进程是否仍然有未释放的 Seccomp 过滤器。 - 如果存在未释放的过滤器,打印警告。
9.11.4. 释放内核相关资源
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
release_user_cpus_ptr(tsk);
scs_release(tsk);
release_user_cpus_ptr
:释放与用户CPU
绑定相关的资源。scs_release
:如果启用了Shadow Call Stack
(CONFIG_SCS
),释放与SCS
相关的资源。
9.11.5. 释放任务栈
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
#ifndef CONFIG_THREAD_INFO_IN_TASK
release_task_stack(tsk);
#else
WARN_ON_ONCE(refcount_read(&tsk->stack_refcount) != 0);
#endif
- 如果任务栈(
task_stack
)与task_struct
分离(未启用CONFIG_THREAD_INFO_IN_TASK
),调用release_task_stack
释放任务栈。 - 如果任务栈与
task_struct
合并(启用了CONFIG_THREAD_INFO_IN_TASK
),检查任务栈的引用计数是否为零。如果不为零,打印警告。
[注]:在 x86_64 中我们之前已经看到了 CONFIG_THREAD_INFO_IN_TASK 宏是默认开启的,即任务栈(thread_info)与 task_struct 合并,因此这里会继续向下执行不会执行 release_task_stack 函数。
9.11.6. 释放相关调试信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
rt_mutex_debug_task_free(tsk);
ftrace_graph_exit_task(tsk);
- 释放与实时互斥锁(
rt_mutex
)相关的调试信息。 - 释放与
Ftrace
图跟踪相关的资源。
9.11.7. 释放架构特定的任务结构
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
arch_release_task_struct(tsk);
- 调用架构特定的函数释放与任务结构相关的资源。
9.11.8. 释放内核线程结构
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
if (tsk->flags & PF_KTHREAD)
free_kthread_struct(tsk);
- 如果进程是内核线程(
PF_KTHREAD
标志被设置),调用free_kthread_struct
释放内核线程的特定资源。
9.11.9. 释放 BPF 任务存储
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
bpf_task_storage_free(tsk);
- 释放与
BPF
(Berkeley Packet Filter
)相关的任务存储。
9.11.10. 释放任务描述符
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: free_task()
free_task_struct(tsk);
- 最终调用
free_task_struct
释放task_struct
的内存。 free_task_struct
的实现如下:// Linux Kernel 6.15.0-rc2 // PATH: kernel/fork.c static inline void free_task_struct(struct task_struct *tsk) { kmem_cache_free(task_struct_cachep, tsk); }
- 使用
kmem_cache_free
从内核缓存中释放task_struct
。(真正释放结束)
- 使用
free_task
调用了多个子函数来释放不同子系统的资源(如任务栈、实时互斥锁、BPF
存储等)。 arch_release_task_struct
提供了一个架构特定的钩子,用于释放与特定硬件架构相关的资源。最终释放 task_struct
的内存。
10. 进程退出为什么延时销毁?
经过这么长的学习分析,我们已经知道了退出一个进程会先调用 exit
系统调用将释放掉该进程的大部分内核资源,并且会调用 exit_notify
使得进程变为僵尸进程留存在系统中,并且会为该进程找到一个合适的养父进程。此时的进程虽然已经将大部分的资源都释放掉了,但仍然保留着部分内核资源(如内核栈和进程描述符)。最终通过系统 RCU
延时释放机制完全回收释放进程所有资源。也可通过父进程调用 wait
系统调用来最终释放掉该进程的所有资源。
先保留到僵尸态,让父进程有机会 wait()
收集子进程的退出码和统计信息;再通过 release_task()
完全摘除所有内核链表/哈希,并释放真正的内存。这样设计可以保证:
- 并发安全
- 进程描述符和内核栈在进程生命周期内,可能被其他内核线程(如调度器、
ptrace
、/proc
读取等)并发访问。 - 直接释放会导致悬挂指针和内存访问错误。
RCU
机制保证在所有并发访问者都“走开”后,才真正释放内存,避免并发访问时释放导致的崩溃。- 父进程总能拿到子进程的最后信息(退出码、资源使用统计、信号状态);
- 高效无锁读取
RCU
允许读者无锁访问数据结构,提高了内核并发性能。- 只有在确定没有读者访问时,才安全地释放资源。
- 直到父进程确认不用后,才彻底回收
task_struct
,避免任何访问竞争。
- 适应复杂的进程关系和引用计数
- 进程描述符和内核栈的引用计数可能在进程退出后才归零(如
/proc
、ptrace
、wait
等场景)。 RCU
延迟释放可以确保所有引用都安全结束后再释放资源。- 直到父进程确认不用后,才彻底回收
task_struct
,避免任何访问竞争。
- 避免死锁和抢占限制
- 某些释放操作不能在原子上下文或持锁情况下直接执行,
RCU
回调会在合适的上下文中异步完成释放,避免死锁和抢占问题。
11. 遗留问题
我们已经一起分析过了一个进程是如何被创建出来的、一个进程又是如何结束生命周期退出任务调度列表并且最终被内核回收和销毁。
我们都知道在用户态程序在创建新进程时候调用 fork()
系统调用后只需要通过函数的返回值来判断当前进程是父进程还是子进程,返回值为 0
表示子进程,返回值大于零也就是子进程的 pid
,则表示父进程。
一个进程的结束时,父进程可以使用 wait
系统调用完成进程间同步。子进程使用 eixt()
退出进程运行态,将清理进程的大部分资源,并从运行态转变为僵尸态留存在系统,最终父进程调用 wait()
系统调用同步进程并完全释放子进程的所有资源。
那么这样就产生了两个问题:
- 用户态程序是如何判断
fork()
函数的两个不同的返回值,fork()
是如何实现返回两个不同的返回值这样的机制? - 父进程在子进程
exit()
退出执行后,在什么时机调用wait()
释放子进程的所有资源?
我们将在接下来的文章中继续探讨这两个问题。
12. 处理遗留问题
在之前提到了两个问题分别是:
- 用户态程序是如何判断
fork()
函数的两个不同的返回值,fork()
是如何实现返回两个不同的返回值这样的机制? - 父进程在子进程
exit()
退出执行后,在什么时机调用wait()
释放子进程的所有资源?也就是说,父进程怎么知道子进程什么时候退出运行了。
12.1. 问题 1 的分析
在分析问题之前首先来观察问题是如何产生的,来看一段再平常不过的 fork()
函数使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程路径
printf("This is the child process.\n");
} else if (pid > 0) {
// 父进程路径
printf("This is the parent process. Child PID: %d\n", pid);
} else {
// 错误处理
perror("fork failed");
}
return 0;
}
编译并执行这段代码得到的输出如下:
$ ./a.out
This is the parent process. Child PID: 135134
This is the child process.
这段程序所作的事情也很简单,就是在主进程中使用 fork()
函数开始创建子进程,接着根据 fork()
函数的返回值来判断:
- 返回值等于
0
:则当前为子进程; - 返回值大于
0
:则当前为主进程(父进程),这时的返回值为子进程的PID
; - 返回值小于
0
:则说明进程创建失败。
代码的处理逻辑很简单,但是应该如何理解通过一个返回值判断不同多种状态,且进入了多条分支这件事呢?说白了就是,fork()
函数的返回值保存进 pid
变量,首先在当前进程中,这个变量只能保存一个确切的值,那么这里是如何又执行了 if
条件中的代码,同时也执行了 else if
条件中的代码呢?带着这个问题我们回到之前所梳理 fork()
函数工作过程中。
12.2. 回顾 fork()
根据上面的函数调用图简要的回顾以下 fork()
函数的工作流程,其核心部分就是 copy_process()
函数了,该函数会为进程分配具体的描述符内存(task_struct
),并作相关的初始化工作,其原理就是利用当前进程的信息拷贝作为一个新的进程存在,而在这其中较为关键的一步就是调用 copy_thread()
函数拷贝父进程的上下文(内核栈、寄存器等)。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:copy_process()
retval = copy_thread(p, args);
if (retval)
goto bad_fork_cleanup_io;
12.3. copy_thread() & kernel_clone()
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/kernel/process.c
int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
{
...
// 复制父进程的寄存器上下文到子进程。
*childregs = *current_pt_regs();
childregs->ax = 0;
if (sp)
childregs->sp = sp;
...
}
copy_thread
函数会为子进程拷贝父进程的信息,其中包含父进程寄存器状态,current_pt_regs
函数便是这个作用,而当拷贝完父进程寄存器后,紧接着将 AX
寄存器的值修改为 0
。而在 x86_64
架构中,AX
寄存器正是作为返回值寄存器来使用,从此也就注定子进程给的返回值将是 0
。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
pid_t kernel_clone(struct kernel_clone_args *args)
{
...
// 获取子进程 pid 结构体
pid = get_task_pid(p, PIDTYPE_PID);
// 获取子进程 pid 值
nr = pid_vnr(pid);
...
put_pid(pid);
return nr;
}
而在 kernel_clone
函数中则最终将获取子进程的 pid
作为返回值,这也就是为什么父进程需要用返回值大于零来判断了。也就是说,当父进程执行完 copy_thread
函数后,在内存中存在了一个除了 ax
寄存器值不同外其它和自己一模一样的进程,当然也包含 pc
寄存器状态也被复制到子进程,当父进程执行到了 wake_up_new_task
函数时,子进程将会从当时拷贝的 pc
处投入运行,这时候也就是在当前父进程更早的代码处执行(copy_thread
阶段)。提到这一点,再回头看一下用户程序代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程路径
printf("This is the child process.\n");
} else if (pid > 0) {
// 父进程路径
printf("This is the parent process. Child PID: %d\n", pid);
} else {
// 错误处理
perror("fork failed");
}
return 0;
}
$ ./a.out
This is the parent process. Child PID: 135134
This is the child process.
在代码中分明是先判断 pid == 0
是否是子进程,后判断是否是父进程,假设父子进程 fork()
结束后是从同位置开始运行,那么按照代码逻辑应该是先打印子进程打输出而不是父进程的输出。但是时机编译并执行这段代码得到的输出则是先去执行 else if
中父进程的打印,而后执行子进程的打印,这更进一步的验证了笔者的分析,即子进程的运行时机是在父进程更早的指令处开始执行。下图展示了笔者对整个分析过程中内存中的变化,以及父子进程执行时机的指令顺序和函数返回值。好的,那么问题 1
到这里也就回答完毕了。
12.4. 问题 2 的分析
子进程如何通知父进程自己结束运行了呢?其实这句话是否在哪里见过,没错,在分析 exit
系统调用时,其中 do_exit
函数调用了一个 exit_notify
的函数,当时笔者讲该函数会通知父进程当前进程已退出,那么具体来看看是如何通知,以及通知了什么。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/exit.c
static void exit_notify(struct task_struct *tsk, int group_dead)
{
bool autoreap;
struct task_struct *p, *n;
LIST_HEAD(dead);
// 加写锁保护全局进程链表,防止并发修改。
write_lock_irq(&tasklist_lock);
// 将当前进程的所有子进程重新指定新父进程(如 init 或子收养者),并处理 ptrace 相关的子进程。如果有需要释放的子进程,加入 dead 链表。
forget_original_parent(tsk, &dead);
// 如果整个线程组都已退出,检查是否有孤儿进程组需要发送 SIGHUP/SIGCONT 信号。
if (group_dead)
kill_orphaned_pgrp(tsk->group_leader, NULL);
// 将当前进程的 exit_state 设为 EXIT_ZOMBIE,表示进程已退出,等待父进程回收。
tsk->exit_state = EXIT_ZOMBIE;
if (unlikely(tsk->ptrace)) {
int sig = thread_group_leader(tsk) &&
thread_group_empty(tsk) &&
!ptrace_reparented(tsk) ?
tsk->exit_signal : SIGCHLD;
autoreap = do_notify_parent(tsk, sig);
} else if (thread_group_leader(tsk)) {
autoreap = thread_group_empty(tsk) &&
do_notify_parent(tsk, tsk->exit_signal);
} else {
autoreap = true;
/* untraced sub-thread */
do_notify_pidfd(tsk);
}
// 如果父进程不关心该进程(autoreap),将 exit_state 设为 EXIT_DEAD,并把该进程加入 dead 链表,稍后释放。
if (autoreap) {
tsk->exit_state = EXIT_DEAD;
list_add(&tsk->ptrace_entry, &dead);
}
/* mt-exec, de_thread() is waiting for group leader */
// 如果有线程在等待 group leader 退出(如多线程 exec),唤醒等待的线程。
if (unlikely(tsk->signal->notify_count < 0))
wake_up_process(tsk->signal->group_exec_task);
// 释放全局进程链表的写锁。
write_unlock_irq(&tasklist_lock);
// 遍历 dead 链表,删除每个进程的 ptrace_entry,并调用 release_task 彻底释放其资源(包括内核栈和 task_struct)。
list_for_each_entry_safe(p, n, &dead, ptrace_entry) {
list_del_init(&p->ptrace_entry);
release_task(p);
}
}
主要来看中间这段代码。
if (unlikely(tsk->ptrace)) {
int sig = thread_group_leader(tsk) &&
thread_group_empty(tsk) &&
!ptrace_reparented(tsk) ?
tsk->exit_signal : SIGCHLD;
autoreap = do_notify_parent(tsk, sig);
} else if (thread_group_leader(tsk)) {
autoreap = thread_group_empty(tsk) &&
do_notify_parent(tsk, tsk->exit_signal);
} else {
autoreap = true;
/* untraced sub-thread */
do_notify_pidfd(tsk);
}
- 通知父进程:
- 被 ptrace 跟踪:如果当前进程被
ptrace
跟踪,选择合适的信号(通常是SIGCHLD
或exit_signal
),调用do_notify_parent
通知父进程,并判断是否需要自动回收(autoreap
)。 - 线程组组长:如果是程组组长,且线程组已空,通知父进程,并判断是否需要自动回收。
- 普通子线程:直接设置
autoreap = true
,并通知pidfd
(如果有)。
- 被 ptrace 跟踪:如果当前进程被
也就是说,无论如何都会通知父进程一个信号,要么是 SIGCHLD
,要么就是 tsk->exit_signal
。那么就来看 exit_signal
是什么吧,从其实追查到 exit
系统调用的定义位置并没有发现对进程 exit_signal
的赋值位置,从进程创建后就不会主动改变 exit_signal
的值。那么就直接追查到进程创建时。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
struct kernel_clone_args args = {
.exit_signal = SIGCHLD,
};
return kernel_clone(&args);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
很好,我们在 fork
系统调用中看到了为 exit_signal
赋值的过程,原来这里给的信号也是 SIGCHLD
,那就不用多说什么了,在 exit_notify
时发送给父进程的信号毫无疑问就是 SIGCHLD
。也就是说,当父进程收到了 SIGCHLD
信号就表示子进程正在执行 do_exit
函数中的 exit_notify
函数向自己发送信号系统通知父进程自己退出运行了。
12.5. 验证问题 2 的分析
根据上面的分析,也就清楚了在进程退出后会向父进程发送 SIGCHLD
信号,那么也就是当父进程收到 SIGCHLD
信号时就可以调用 wait
主动释放子进程了。接下来用一段代码来验证这件事:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void sigchld_handler(int signo) {
int status;
pid_t pid;
// 使用 waitpid 处理所有已终止的子进程
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("[Get:SIGCHLD]子进程 %d 已终止\n", pid);
}
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 确保被中断的系统调用自动重启
sigaction(SIGCHLD, &sa, NULL);
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程运行中...\n");
sleep(2);
exit(0);
} else if (pid > 0) {
// 父进程
printf("父进程等待子进程终止...\n");
pause(); // 等待信号
} else {
perror("fork");
exit(1);
}
return 0;
}
编译并执行上面代码将会得到这样的输出:
$ ./a.out
父进程等待子进程终止...
子进程运行中...
[Get:SIGCHLD]子进程 142052 已终止
可以看到子进程结束后,父进程的确收到了 SIGCHLD
信号,由此父进程就可以通过该信号了解子进程是否退出运行了。
12.6. 小结
那么到此两个遗留问题也就分析结束了,有关《Linux 内核设计与实现》第三章内容中进程部分也就基本被我们剖析的差不多了。是的,还有一个点,那就是线程,让我们继续来学习吧。
13. 线程
线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术(concurrent programming
),在多处理器系统上,它也能保证真正的并行处理(parallelism
)。Linux
实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux
把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct
,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。
上述线程机制的实现与 Microsoft Windows
或是 Sun Solaris
等操作系统的实现差异非常大。这些系统都在内核中提供了专门支持线程的机制(这些系统常常把线程称作轻量级进程(lightweight processes
))。“轻量级进程”这种叫法本身就概括了 Linux
在此处与其他系统的差异。在其他的系统中,相较于重量级的进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于 Linux
来说,它只是一种进程间共享资源的手段( Linux
的进程本身就够轻量级了)。举个例子来说,假如我们有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有一个包含指向四个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反,Linux
仅仅创建四个进程并分配四个普通的 task_sturct
结构。建立这四个进程时指定他们共享某些资源,这是相当高雅的做法。
14. 创建线程
通常我们在用户空间创建线程时都会用到一个接口函数 pthread_create
来创建用户级线程,而这个函数是 glibc
中提供给我们的一个封装好的库函数,而真正创建线程的系统调用是 clone
,pthread_create
也是使用 clone
系统调用创建线程。
[注]:这里笔者特意强调 用户级线程,因为后面还会有内核线程的概念。
14.1. clone 系统调用(用户级线程)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
{
struct kernel_clone_args args = {
.flags = (lower_32_bits(clone_flags) & ~CSIGNAL),
.pidfd = parent_tidptr,
.child_tid = child_tidptr,
.parent_tid = parent_tidptr,
.exit_signal = (lower_32_bits(clone_flags) & CSIGNAL),
.stack = newsp,
.tls = tls,
};
return kernel_clone(&args);
}
实际上你发现了,clone
系统调用仅仅只是设置一个 kernel_clone_args
结构体传入给 kernel_clone
去执行。欸~ 发现了吧~ 没错,这是我们熟悉的 kernel_clone
函数,剩下的步骤就和之前创建进程的过程一模一样了。这里不同的就是在 clone
系统调用传入的参数中 clone_flags
通常会指定更多的共享资源(如 CLONE_VM
、CLONE_FS
、CLONE_FILES
、CLONE_THREAD
等),而 fork
函数则是不共享资源。
特性 | fork | clone |
---|---|---|
资源共享 | 子进程与父进程不共享资源。 | 通过 clone_flags 指定共享的资源。 |
灵活性 | 固定行为,无法定制资源共享方式。 | 非常灵活,可以指定共享的资源类型。 |
创建线程 | 不支持创建线程,只能创建独立的进程。 | 可以通过 CLONE_THREAD 创建线程。 |
退出信号 | 默认发送 SIGCHLD 信号给父进程。 | 可以通过 clone_flags 指定退出信号。 |
系统调用参数 | 无参数,固定行为。 | 参数丰富,可以指定栈地址、TLS 等。 |
使用场景 | 创建独立的子进程。 | 创建线程或共享资源的子进程。 |
而这里你应该也会发现,在使用 clone
系统调用时,其实并没有传入线程的入口函数,事实上用于管理用户级线程启动、入口函数的工作都在 pthread_create
中实现好了,换句话说,也就是 glibc
库帮我们实现了这一步骤,因此此处就无需多虑。
14.2. 内核线程
内核经常需要在后台执行一些操作。这种任务可以通过内核线程(kernel thread
)完成独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的 mm
指针被设置为 NULL
)。它们只在内核空间运
行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。Linux
确实会把一些任务交给内核线程去做,像 flush
和ksofirgd
这些任务就是明显的例子。在装有 Linux
系统的机器上运行 ps -ef
命令,你可以看到内核线程,有很多!这些线程在系统启动时由另外一些内核线程创建。实际上,内核线程也只能由其他内核线程创建。
kthread_create
是 Linux 内核中用于创建内核线程的高级接口。内核是通过从kthreadd
内核进程中衍生出所有新的内核线程来自动处理这一点的。
14.3. 创建内核线程
14.3.1. kthread_create
kthread_create
是 Linux
内核用于创建内核线程的常用接口,其作用是创建一个新的内核线程,并返回对应的 task_struct
指针,但不会立即启动该线程。如果需要唤醒线程则需要调用 wake_up_process
。
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/kthread.h
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
- threadfn:新线程启动后要执行的函数指针(入口函数)。
- data:传递给入口函数的参数。
- namefmt/arg…:线程名的格式字符串和参数(类似
printf
)。
14.3.2. kthread_run
正如上面所说,kthread_create
会创建一个内核线程,但并不会立刻执行,而需要手动唤醒,内核当然也有另一个接口函数 kthread_run
用于创建一个线程并立即执行它。其实就是对 kthread_create
和 wake_up_process
的一个封装。
// Linux Kernel 6.15.0-rc2
// PATH: include/linux/kthread.h
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
14.3.3. kthread_create_on_node
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data, int node,
const char namefmt[],
...)
{
struct task_struct *task;
va_list args;
va_start(args, namefmt);
task = __kthread_create_on_node(threadfn, data, node, namefmt, args);
va_end(args);
return task;
}
threadfn
:新线程启动后要执行的函数指针(入口函数)。data
:传递给入口函数的参数。node
:NUMA
节点,决定线程栈等资源分配在哪个节点。namefmt
:线程名格式字符串(可变参数)。
可以看到该函数的实际工作是由 __kthread_create_on_node
完成,最终返回创建好的 task_struct
。
14.3.4. __kthread_create_on_node
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
void *data, int node,
const char namefmt[],
va_list args)
{
DECLARE_COMPLETION_ONSTACK(done);
struct task_struct *task;
struct kthread_create_info *create = kmalloc(sizeof(*create),
GFP_KERNEL);
if (!create)
return ERR_PTR(-ENOMEM);
create->threadfn = threadfn;
create->data = data;
create->node = node;
create->done = &done;
create->full_name = kvasprintf(GFP_KERNEL, namefmt, args);
if (!create->full_name) {
task = ERR_PTR(-ENOMEM);
goto free_create;
}
spin_lock(&kthread_create_lock);
//将新线程的创建信息排队,等待 kthreadd 内核线程处理。
list_add_tail(&create->list, &kthread_create_list);
spin_unlock(&kthread_create_lock);
// 唤醒 kthreadd 内核线程
wake_up_process(kthreadd_task);
/*
* Wait for completion in killable state, for I might be chosen by
* the OOM killer while kthreadd is trying to allocate memory for
* new kernel thread.
*/
// 等待 kthreadd 创建完成这个内核线程
if (unlikely(wait_for_completion_killable(&done))) {
/*
* If I was killed by a fatal signal before kthreadd (or new
* kernel thread) calls complete(), leave the cleanup of this
* structure to that thread.
*/
if (xchg(&create->done, NULL))
return ERR_PTR(-EINTR);
/*
* kthreadd (or new kernel thread) will call complete()
* shortly.
*/
wait_for_completion(&done);
}
// 获得创建完成的内核线程的 task
task = create->result;
free_create:
kfree(create);
return task;
}
14.3.4.1. 分配并初始化 kthread_create_info
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: __kthread_create_on_node
struct kthread_create_info *create = kmalloc(sizeof(*create), GFP_KERNEL);
create->threadfn = threadfn;
create->data = data;
create->node = node;
create->done = &done;
create->full_name = kvasprintf(GFP_KERNEL, namefmt, args);
分配并初始化一个 kthread_create_info
结构体,保存入口函数、参数、节点、线程名等信息。
14.3.4.2. 加入创建队列并唤醒 kthreadd
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: __kthread_create_on_node
spin_lock(&kthread_create_lock);
list_add_tail(&create->list, &kthread_create_list);
spin_unlock(&kthread_create_lock);
wake_up_process(kthreadd_task);
- 将创建请求加入全局队列
kthread_create_list
。 - 唤醒内核线程
kthreadd_task
,由它来实际创建新线程。
14.3.4.3. 等待线程创建完成
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: __kthread_create_on_node
wait_for_completion_killable(&done);
task = create->result;
kfree(create);
return task;
- 当前调用者阻塞等待,直到新线程创建完成(通过
completion
机制)。
14.3.5 用于创建内核线程的管理线程 kthreadd
kthreadd_task
是定义在 kernel/kthread.c
中的一个全局变量,用于指向 kthreadd
,而 kthreadd
是内核线程的管理线程,是所有内核线程的“工厂”,负责统一、安全地创建内核线程。其本身是在内核启动早期由 kernel_start
通过 kernel_thread
创建得到。
// Linux Kernel 6.15.0-rc2
// PATH: init/main.c
start_kernel()
│
└─> rest_init()
│
└─> kernel_thread(kthreadd,...)
而 kthreadd_task
全局变量的赋值位置也是在 rest_init
函数中:
// Linux Kernel 6.15.0-rc2
// PATH: init/main.c
static noinline void __ref __noreturn rest_init(void)
{
...
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
...
}
虽然现在还没有说明,不过我们也知道每个线程都会有一个入口函数,而 kthreadd
线程的入口函数就是 kthreadd
函数。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
int kthreadd(void *unused)
{
static const char comm[TASK_COMM_LEN] = "kthreadd";
struct task_struct *tsk = current;
/* Setup a clean context for our children to inherit. */
set_task_comm(tsk, comm);
ignore_signals(tsk);
set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_TYPE_KTHREAD));
set_mems_allowed(node_states[N_MEMORY]);
current->flags |= PF_NOFREEZE;
cgroup_init_kthreadd();
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
spin_unlock(&kthread_create_lock);
// 创建内核线程
// kthreadd 内核线程会调用 create_kthread() 函数
create_kthread(create);
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0;
}
kthreadd
线程的函数体就是这里的 kthreadd
函数,其中定义的死循环部分(作为守护线程一直运行)就作为被创建之后的主要工作内容了。它会首先将自己设置为 TASK_INTERRUPTIBLE
,遍历 kthread_create_list
,如果由存在等待被创建的内核线程则调用 create_kthread(create)
创建新线程,若没有将会调用 schedule
切出自己保持睡眠状态以节省 CPU
资源。
14.3.6. create_kthread
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
static void create_kthread(struct kthread_create_info *create)
{
int pid;
#ifdef CONFIG_NUMA
current->pref_node_fork = create->node;
#endif
// 使用 kernel_thread 创建线程,入口函数为 kthread。
/* We want our own signal handler (we take no signals by default). */
pid = kernel_thread(kthread, create, create->full_name,
CLONE_FS | CLONE_FILES | SIGCHLD);
if (pid < 0) {
/* Release the structure when caller killed by a fatal signal. */
struct completion *done = xchg(&create->done, NULL);
kfree(create->full_name);
if (!done) {
kfree(create);
return;
}
create->result = ERR_PTR(pid);
complete(done);
}
}
create_kthread
负责实际分配和启动内核线程,设置 NUMA
亲和性、线程名、入口函数和参数。通过 kernel_thread
创建一个新线程:
- 新线程的入口函数为
kthread
,参数为create
(包含用户指定的threadfn
、data
、线程名等)。 - 线程名用于调试和管理。
- 线程继承文件系统和文件描述符(
CLONE_FS | CLONE_FILES
),并在退出时发送SIGCHLD
。
14.3.7. kernel_thread
kernel_thread
是 Linux
内核用于创建内核线程的标准接口。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
pid_t kernel_thread(int (*fn)(void *), void *arg, const char *name,
unsigned long flags)
{
struct kernel_clone_args args = {
.flags = ((lower_32_bits(flags) | CLONE_VM |
CLONE_UNTRACED) & ~CSIGNAL),
.exit_signal = (lower_32_bits(flags) & CSIGNAL),
.fn = fn,
.fn_arg = arg,
.name = name,
.kthread = 1,
};
return kernel_clone(&args);
}
14.3.7.1. 参数说明
- fn:新线程启动后要执行的入口函数。
- arg:传递给入口函数的参数。
- name:线程名(可选)。
- flags:
clone
标志(如调度、资源继承等)。
14.3.7.2. 构造 kernel_clone_args
- 设置
flags
,强制包含CLONE_VM | CLONE_UNTRACED
,去除CSIGNAL
位。 - 设置
exit_signal
,通常为0
(内核线程不需要信号退出)。 - 设置入口函数
fn
和参数fn_arg
。 - 设置线程名和
kthread
标志。
14.3.7.3. 调用 kernel_clone(🌟🌟🌟🌟🌟)
kernel_clone
是内核统一的进程/线程创建接口。- 其内部会调用
copy_process
,并最终调用copy_thread
。
- 事实上在创建新线程或进程时,内核都会为其设置好“返回地址”为
ret_from_fork_asm
(汇编入口)。
- 在
copy_thread
中,检测到args->fn
非空(即为内核线程),会调用kthread_frame_init
,把入口函数和参数写入新线程的内核栈帧。
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/kernel/process.c
// Function: copy_thread
...
frame->ret_addr = (unsigned long) ret_from_fork_asm;
...
/* Kernel thread ? */
if (unlikely(p->flags & PF_KTHREAD)) {
p->thread.pkru = pkru_get_init_value();
memset(childregs, 0, sizeof(struct pt_regs));
kthread_frame_init(frame, args->fn, args->fn_arg);
return 0;
}
...
- 新线程或进程被调度后,
ret_from_fork_asm
(汇编入口),它最终会通过call ret_from_fork
跳转到C
函数ret_from_fork
,从ret_from_fork
进入。
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/entry/entry_64.S
SYM_CODE_START(ret_from_fork_asm)
/*
* This is the start of the kernel stack; even through there's a
* register set at the top, the regset isn't necessarily coherent
* (consider kthreads) and one cannot unwind further.
*
* This ensures stack unwinds of kernel threads terminate in a known
* good state.
*/
UNWIND_HINT_END_OF_STACK
ANNOTATE_NOENDBR // copy_thread
CALL_DEPTH_ACCOUNT
movq %rax, %rdi /* prev */
movq %rsp, %rsi /* regs */
movq %rbx, %rdx /* fn */
movq %r12, %rcx /* fn_arg */
call ret_from_fork
/*
* Set the stack state to what is expected for the target function
* -- at this point the register set should be a valid user set
* and unwind should work normally.
*/
UNWIND_HINT_REGS
#ifdef CONFIG_X86_FRED
ALTERNATIVE "jmp swapgs_restore_regs_and_return_to_usermode", \
"jmp asm_fred_exit_user", X86_FEATURE_FRED
#else
jmp swapgs_restore_regs_and_return_to_usermode
#endif
SYM_CODE_END(ret_from_fork_asm)
ret_from_fork
检查fn
是否非空(也就是判断是否是内核线程),如果是,则直接调用fn(fn_arg)
,即执行你指定的内核线程入口函数(内核默认指定的kthread
函数)。
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/kernel/process.c
__visible void ret_from_fork(struct task_struct *prev, struct pt_regs *regs,
int (*fn)(void *), void *fn_arg)
{
schedule_tail(prev);
/* Is this a kernel thread? */
if (unlikely(fn)) {
fn(fn_arg);
/*
* A kernel thread is allowed to return here after successfully
* calling kernel_execve(). Exit to userspace to complete the
* execve() syscall.
*/
regs->ax = 0;
}
syscall_exit_to_user_mode(regs);
}
事实上这里有一个细节地方,笔者在进程创建阶段忽略了,那就是这里的 syscall_exit_to_user_mode
函数。
创建进程时:
- 对于普通进程(不是内核线程),
fn
为NULL
,所以不会进入if
(unlikely(fn)
) 分支。只会执行syscall_exit_to_user_mode
。 - 这一步会根据
regs
(新进程的pt_regs
,已经在copy_thread
阶段设置好)恢复用户空间的寄存器上下文,并跳转回用户空间,继续执行用户代码。而这里返回到用户空间的位置也就是新进程从父进程fork
时的用户空间指令位置继续执行。
创建内核线程时:
- 对于内核线程,
fn
非空,直接调用你传入的入口函数(如果是使用kthread_create
创建,那么这里将为默认的kthread
函数,最终这个函数会去调用我们指定的线程入口函数)。 - 而此时的
syscall_exit_to_user_mode
就没有任何意义了,对于内核线程,由于它们没有用户空间上下文(regs->ip
、regs->sp
都为0
),所以这个函数不会真正跳转到用户空间。内核线程的入口函数一旦返回,线程就会退出(通常调用do_exit()
),不会进入用户空间。
14.3.8. kthread 入口
- 新线程启动后,通过
ret_from_fork
将会执行kthread(create)
,从create
结构体中取出threadfn
和data
,并最终调用threadfn(data)
,即我们指定的入口函数。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
static int kthread(void *_create)
{
static const struct sched_param param = { .sched_priority = 0 };
/* Copy data: it's on kthread's stack */
struct kthread_create_info *create = _create;
int (*threadfn)(void *data) = create->threadfn;
void *data = create->data;
struct completion *done;
struct kthread *self;
int ret;
// 将当前线程的 task_struct 转换为 kthread 结构
self = to_kthread(current);
// xchg 用于交换 create->done 的值,确保同步完成。
/* Release the structure when caller killed by a fatal signal. */
done = xchg(&create->done, NULL);
// 如果 done 为 NULL,说明创建线程的调用者已被杀死,线程直接退出。
if (!done) {
kfree(create->full_name);
kfree(create);
kthread_exit(-EINTR);
}
self->full_name = create->full_name;
self->threadfn = threadfn;
self->data = data;
/*
* The new thread inherited kthreadd's priority and CPU mask. Reset
* back to default in case they have been changed.
*/
// 设置调度策略为 SCHED_NORMAL,优先级为 0。
sched_setscheduler_nocheck(current, SCHED_NORMAL, ¶m);
/* OK, tell user we're spawned, wait for stop or wakeup */
__set_current_state(TASK_UNINTERRUPTIBLE);
create->result = current;
/*
* Thread is going to call schedule(), do not preempt it,
* or the creator may spend more time in wait_task_inactive().
*/
// 禁用内核抢占,确保当前线程不会被调度器切换出去。
preempt_disable();
// 调用 complete 完成创建线程的信号量,通知创建线程已完成。
// 这里的 done 是一个信号量,表示线程创建完成。
complete(done);
// 调用 schedule_preempt_disabled 让出 CPU,当前线程睡眠,等待被唤醒。
schedule_preempt_disabled();
// 恢复内核抢占,允许调度器正常运行。
preempt_enable();
self->started = 1;
if (!(current->flags & PF_NO_SETAFFINITY) && !self->preferred_affinity)
kthread_affine_node(); // 调用 kthread_affine_node 将线程绑定到 NUMA 节点。
ret = -EINTR;
// 如果线程未设置 KTHREAD_SHOULD_STOP 标志,则调用用户指定的线程函数 threadfn(data)。
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
cgroup_kthread_ready();
__kthread_parkme(self);
ret = threadfn(data);
}
// 用 kthread_exit 退出线程,并将返回值传递给 kthread_stop。
kthread_exit(ret);
}
14.3.8.1. 入口参数与初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: kthread
static const struct sched_param param = { .sched_priority = 0 };
struct kthread_create_info *create = _create;
int (*threadfn)(void *data) = create->threadfn;
void *data = create->data;
struct completion *done;
struct kthread *self;
int ret;
_create
是kthread_create_info
结构体,包含入口函数、参数、线程名等。- 取出用户指定的入口函数
threadfn
和参数data
。
14.3.8.2. 线程结构体初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: kthread
self = to_kthread(current);
done = xchg(&create->done, NULL);
if (!done) {
kfree(create->full_name);
kfree(create);
kthread_exit(-EINTR);
}
self->full_name = create->full_name;
self->threadfn = threadfn;
self->data = data;
- 将线程名、入口函数、参数等保存到当前线程的
struct kthread
结构体。 - 如果
done
为NULL
,说明创建者线程已被杀死,直接退出。
14.3.8.3. 调度策略与同步
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: kthread
sched_setscheduler_nocheck(current, SCHED_NORMAL, ¶m);
__set_current_state(TASK_UNINTERRUPTIBLE);
create->result = current;
preempt_disable();
complete(done);
schedule_preempt_disabled();
preempt_enable();
self->started = 1;
- 设置调度策略为普通优先级。
- 设置当前线程为不可中断睡眠,通知创建者线程(通过
complete(done)
)线程已创建完成。 - 让出
CPU
,内核线程主动进入睡眠(schedule_preempt_disabled()
),等待被唤醒。 - 唤醒后,标记线程已启动。
14.3.8.4. NUMA 亲和性
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: kthread
if (!(current->flags & PF_NO_SETAFFINITY) && !self->preferred_affinity)
kthread_affine_node();
- 如果没有特殊亲和性,绑定到指定
NUMA
节点。
14.3.8.5. 线程主循环与入口函数调用
// Linux Kernel 6.15.0-rc2
// PATH: kernel/kthread.c
// Function: kthread
ret = -EINTR;
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
cgroup_kthread_ready();
__kthread_parkme(self);
ret = threadfn(data);
}
kthread_exit(ret);
}
- 如果没有设置停止标志,调用
cgroup_kthread_ready()
做cgroup
相关准备。 - 调用
__kthread_parkme(self)
,如果线程需要park
,会进入park
状态等待唤醒。 - 调用用户指定的入口函数
threadfn(data)
,并将返回值作为线程退出码。(这里所执行的threadfn
就是在创建内核线程时我们所指定的入口函数了。) - 调用
kthread_exit(ret)
退出线程,kthread_exit(ret)
会唤醒等待kthread_stop()
的线程,并调用do_exit()
彻底退出。
14.4. 内核线程创建的典型用法
static int my_thread_func(void *data) {
// 线程主循环
while (!kthread_should_stop()) {
// do work...
}
return 0;
}
struct task_struct *tsk = kthread_create(my_thread_func, NULL, "my_kthread");
wake_up_process(tsk); // 启动线程
或直接:
struct task_struct *tsk = kthread_run(my_thread_func, NULL, "my_kthread");
14.5 小结
kthread_create
创建一个内核线程,设置入口函数和参数,但不会自动启动。- 线程启动后自动执行你指定的函数,返回时线程结束。
- 推荐用
kthread_run
创建并立即启动线程。
使用 kthread_create
这个相对高级的封装接口去创建内核线程并不会主动运行,而需要等待被唤醒后才可运行,而需要创建后立刻执行可以使用 kthread_run
来创建,二者最终都会依靠唤醒 threadd
线程来创建新的内核线程,而 threadd
线程会调用 create_kthread
函数构造并初始化参数结构体(其中传入的入口函数为 kthread
),再去调用 kernel_clone
我们熟知的创建进程和线程的函数。
这里有一个比较细节的地方不知道各位有没有发现产生疑问的,那就是我们知道通过 kthread_create
创建出来的线程不会主动运行,那是因为在 kthread
函数中会将当前线程状态改为 TASK_UNINTERRUPTIBLE
,然后主动让出 CPU
进入睡眠状态,等待被唤醒。而这时候在 kernel_clone
函数中,创建完线程后,也就是执行完 copy_process
创建出新线程后会继续执行 wake_up_new_task
函数,我们都知道这个函数是用来唤醒线程或进程的,那么这时候为什么最终创建出来的线程还是睡眠状态并没有被唤醒呢?
实际上,如果有读者会有这样的疑问的话,那其实是说明并没有完全理解整个创建的工作流程导致的,这其中有着一定的顺序关系。也就是说当 kernel_clone
函数通过 copy_process
函数创建出线程后,这时候仅仅只是在内存中将它分配并初始化出来,并不能直接投入运行,也就是说线程并不能通过 kernel_clone
设置的返回入口 ret_from_fork
去执行。而当执行到 kernel_clone
的 wake_up_new_task
时候,这才让该线程有了执行能力,CPU
去调度它运行,从 ret_from_fork
入口执行,先是调用 kthread
函数,而 kthread
函数会讲线程的入口函数设置为 kthreadfn
也就是最开始调用函数时候由我们主动传入的自定义函数,在设置完成后由将当前线程状态设置为 TASK_UNINTERRUPTIBLE
不可被中断状态,接着就调用 schedule_preempt_disabled
让出 CPU
,进入睡眠状态。这时候这个线程也就完成了所有的创建步骤,当我们主动调用 wake_up_process
函数时它才会恢复运行,去执行我们指定的函数,执行完成后将会调用 kthread_exit
完成退出,当然它也是利用 do_exit
函数实现的。
[注]:由于之前已经详细画过 kernel_clone 函数,这里就简要画出关键步骤。
15. 第三章总结
不知不觉间我们已经完成整个第三章内容的学习,在本章中,我们学习了操作系统中的核心概念–进程。我们也讨论了进程的一般特性,它为何如此重要,以及进程与线程之间的关系。然后,讨论了Linux
如何存放和表示进程(用 task_stnuct
和 thread_info
),如何创建进程(通过 fork()
),实际上最终是 kernel_clone()
),如何表示进程的层次关系,父进程又是如何收集其后代的信息(通过 wait()
系统调用族),以及进程最终如何消亡(强制或自地调用 exit()
))。进程是一个非常基础、非常关键的抽象概念,位于每一种现代操作系统的核心位置,也是我们拥有操作系统(用来运行程序)的最终原因。第 4
章讨论进程调度,内核以这种微妙而有趣的方式来决定哪个进程运行,何时运行,以何种顺序运行。
#附
#01. 计算 task_struct 大小
方法:通过编写一个简单的内核模块,用 sizeof()
去计算 struct task_struct
结构体大小并打印出来。
#01.1. 编写 task_struct_size 模块
编写内核模块文件 task_struct_size.c
。
$ vim task_struct_size.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>
static int __init task_struct_size_init(void)
{
pr_info("Size of task_struct: %zu bytes\n", sizeof(struct task_struct));
return 0;
}
static void __exit task_struct_size_exit(void)
{
pr_info("Exiting task_struct size module\n");
}
module_init(task_struct_size_init);
module_exit(task_struct_size_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Imagine Miracle");
MODULE_DESCRIPTION("Module to calculate task_struct size");
#01.2. 编写 Makefile
编写 Makefile
文件:
obj-m += task_struct_size.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
#01.3. 编译并插入模块
执行 make
编译模块代码,生成 .ko
文件。
$ make
# 编译完成将会生成如下文件
$ ls
Makefile Module.symvers task_struct_size.ko task_struct_size.mod.c task_struct_size.o
modules.order task_struct_size.c task_struct_size.mod task_struct_size.mod.o
插入内核。
$ sudo insmod task_struct_size.ko
# 可以通过 lsmod 命令查看是否插入成功
$ lsmod | grep task_struct_size
task_struct_size 12288 0
#01.4. 查看内核打印
执行 dmesg
命令查看内核输出:
$ sudo dmesg
......
[113194.864193] Size of task_struct: 13760 bytes
一般情况下,最后一行输出就是刚刚插入的内核模块的打印信息,这里可以看到通过 sizeof
所计算出来的 task_struct
大小为 13760 bytes
,约等于 13.43 KB
。
卸载该模块:
$ sudo rmmod task_struct_size
#02. 验证内核栈大小
继续使用 #01
部分的内核模块代码,来添加几行打印:
$ vim task_struct_size.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>
static int __init task_struct_size_init(void)
{
pr_info("Size of task_struct: %zu bytes\n", sizeof(struct task_struct));
pr_info("Size of thread_union: %zu bytes\n", sizeof(union thread_union));
pr_info("Size of THREAD_SIZE: %zu bytes\n", THREAD_SIZE);
pr_info("Size of PAGE_SIZE: %zu bytes\n", PAGE_SIZE);
pr_info("Size of KASAN_STACK_ORDER: %zu bytes\n", KASAN_STACK_ORDER);
return 0;
}
static void __exit task_struct_size_exit(void)
{
pr_info("Exiting task_struct size module\n");
}
module_init(task_struct_size_init);
module_exit(task_struct_size_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Imagine Miracle");
MODULE_DESCRIPTION("Module to calculate task_struct size");
插入模块之后,通过 sudo dmesg
查看打印信息如下:
[52373.661330] Size of task_struct: 13760 bytes
[52373.661341] Size of thread_union: 16384 bytes
[52373.661344] Size of THREAD_SIZE: 16384 bytes
[52373.661346] Size of PAGE_SIZE: 4096 bytes
[52373.661349] Size of KASAN_STACK_ORDER: 0 bytes
thread_union
大小与 THREAD_SIZE
相同,为 16348 bytes = 16KB
。
执行 sudo rmmod task_struct_size
移除模块。
$ sudo rmmod task_struct_size
#03. 验证内核栈结构
方法同样还是编写一个简单的内核模块来验证我们的假设。这里是为了验证 task_struct
、thread_info
、thread_union
(内核栈)三个地址是相同的。
编写内核模块文件 task_struct_addr.c
。
$ vim task_struct_addr.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
static int __init print_stack_address_init(void)
{
pr_info("task_struct address: %lx \n", (unsigned long **)current);
pr_info("task_struct->stack(stack base): %lx \n", (unsigned long **)(current->stack));
pr_info("task_struct->thread_info address: %lx \n", (unsigned long *)&(current->thread_info));
return 0;
}
static void __exit print_stack_address_exit(void)
{
pr_info("Exiting module.\n");
}
module_init(print_stack_address_init);
module_exit(print_stack_address_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Imagine Miracle");
MODULE_DESCRIPTION("Print kernel stack address.");
编写 Makefile
文件:
obj-m += task_struct_addr.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
执行 make
编译模块,执行 sudo insmod task_struct_addr
插入模块,并通过 sudo dmesg
命令查看内核打印信息。
[72607.977772] task_struct address: ffff88fd9520a940
[72607.977773] task_struct->stack(stack base): ffffca988fdb0000
[72607.977774] task_struct->thread_info address: ffff88fd9520a940
这里发现三个地址并不相同,而只有 task_struct
与 thread_info
是相同的,这是由于 thread_info
就是 task_struct
第一个成员。那么这里也就是验证了其实内核栈地址并不是如同 thread_union
描述的那样。
#04. 验证栈指针位置
验证方式与以上几次的方式相同,这里就仅展示具体代码和输出结果。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
static int __init print_stack_address_init(void)
{
unsigned long sp = 0;
unsigned long bp = 0;
// 获取当前栈指针
#ifdef CONFIG_X86_64
asm volatile("mov %%rsp, %0" : "=r" (sp)); // x86_64 使用 rsp
asm volatile("mov %%rbp, %0" : "=r" (bp)); // x86_64 使用 rbp
#else
asm volatile("mov %%esp, %0" : "=r" (sp)); // x86_32 使用 esp
asm volatile("mov %%ebp, %0" : "=r" (bp)); // x86_32 使用 ebp
#endif
pr_info("task_struct->stack(stack base): %lx \n", (unsigned long **)(current->stack));
pr_info("task_struct->stack(stack top): %lx \n", (unsigned long **)(current->stack) + THREAD_SIZE);
pr_info("Current stack pointer (sp): %lx\n", sp);
pr_info("Current frame pointer (bp): %lx\n", bp);
return 0;
}
static void __exit print_stack_address_exit(void)
{
pr_info("Exiting module.\n");
}
module_init(print_stack_address_init);
module_exit(print_stack_address_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Imagine Miracle");
MODULE_DESCRIPTION("Print kernel stack address.");
打印结果:
[74029.947307] task_struct->stack(stack base): ffffca9889e84000
[74029.947309] task_struct->stack(stack top): ffffca9889ea4000
[74029.947311] Current stack pointer (sp): ffffca9889e87928
[74029.947313] Current frame pointer (bp): ffffca9889e87940
#05. init 进程
此处待补充