一、引言

在Linux内核的五大组成部分(进程管理、内存管理、设备驱动、文件系统、网络协议)中,进程管理是非常重要的一部分,它虽然不像内存管理、虚拟文件系统那样复杂,也不像进程间通信那样条理化,但作为五大内核组成部分之一,进程管理对我们理解内核的运作、对于我们今后的编程非常重要。同时,作为五大组成部分的核心,它与其他四个模块都有联系。 因此,对于进程管理是必须要充分理解和掌握的。本文对进程管理进行详细的介绍,首先介绍进程及其相关的概念,而后介绍进程的创建、退出等基本操作以及线程的相关知识。

二、背景

进程是Unix/Linux操作系统抽象概念中最基本也是最重要的一种。人们拥有操作系统就是为了运行用户程序,因此,进程管理是所有操作系统的心脏所在,Linux也不例外。由于并非所有人对于Linux的进程管理这一部分知识都充分了解和掌握,因此本文将详细地介绍和讲解Linux进程管理的相关知识,以期读过这边文章的人,能够走进进程管理的大门,并在Linux内核这条大道上越走越远。

三、技术章节

1. 进程

1.1 进程的概念

进程(process)就是处于执行期的程序(目标代码存放在某种存储介质上),其可以理解为程序执行的一个实例。但进程并不仅仅局限于一段可执行程序代码,通常还包括其他资源,如打开的文件,挂起的信号,内核内部数据,处理器住状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,以及用来存放全局变量的数据段等。从内核的角度看,进程也可以称为任务。

1.2 线程的概念

执行线程,简称线程(thread),是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的Unix系统中,一个进程只包含一个线程。但现在的系统中,包含多个线程的多线程程序司空见惯。Linux系统对线程和进程并不作特别区分,对于Linux而言,线程只不过是一种特殊的进程罢了。

1.3 进程的创建

进程在被创建的时刻开始存活。在Linux系统中,这通常通过fork()系统调用来实现。该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程成为父进程,新产生的进程称为子进程。该调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新创建的子进程。

通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()函数族就可以创建新的地址空间,并把新的程序载入其中。在现代Linux内核中,fork()实际上是由clone()系统调用实现的。

1.4 进程的退出

最终,程序通过exit()系统调用退出执行。这个系统调用会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用(一族,包括wait()、waitpid()、wait3()、wait4())查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死(僵尸)状态,直到其父进程调用wait()或waitpid()为止。

2. 进程描述符及任务结构

内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构,该结构定义在include/linux/sched.h文件中。进程描述符中包含一个具体进程的所有信息,源码如下(4.19内核,下同):

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/* -1 unrunnable, 0 runnable, >0 stopped: */volatile long          state;/** This begins the randomizable portion of task_struct. Only* scheduling-critical items should be added above here.*/randomized_struct_fields_startvoid              *stack;atomic_t         usage;/* Per task flags (PF_*), defined further below: */unsigned int           flags;unsigned int          ptrace;#ifdef CONFIG_SMPstruct llist_node       wake_entry;int              on_cpu;
#ifdef CONFIG_THREAD_INFO_IN_TASK/* Current CPU: */unsigned int         cpu;
#endifunsigned 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;
#endifint               on_rq;int               prio;int                static_prio;int             normal_prio;unsigned int            rt_priority;const struct sched_class    *sched_class;struct sched_entity        se;struct sched_rt_entity       rt;
#ifdef CONFIG_CGROUP_SCHEDstruct task_group     *sched_task_group;
#endifstruct sched_dl_entity        dl;#ifdef CONFIG_PREEMPT_NOTIFIERS/* List of struct preempt_notifier: */struct hlist_head       preempt_notifiers;
#endif#ifdef CONFIG_BLK_DEV_IO_TRACEunsigned int            btrace_seq;
#endifunsigned int          policy;int              nr_cpus_allowed;cpumask_t           cpus_allowed;#ifdef CONFIG_PREEMPT_RCUint               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_RCUunsigned long          rcu_tasks_nvcsw;u8              rcu_tasks_holdout;u8                rcu_tasks_idx;int               rcu_tasks_idle_cpu;struct list_head     rcu_tasks_holdout_list;
#endif /* #ifdef CONFIG_TASKS_RCU */struct sched_info       sched_info;struct list_head     tasks;
#ifdef CONFIG_SMPstruct plist_node      pushable_tasks;struct rb_node           pushable_dl_tasks;
#endifstruct mm_struct      *mm;struct mm_struct        *active_mm;/* Per-thread vma caching: */struct vmacache         vmacache;#ifdef SPLIT_RSS_COUNTINGstruct task_rss_stat      rss_stat;
#endifint               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_remote_wakeup:1;/* Force alignment to the next boundary: */unsigned           :0;/* Unserialized, strictly 'current' *//* Bit to tell LSMs we're in execve(): */unsigned           in_execve:1;unsigned            in_iowait:1;
#ifndef TIF_RESTORE_SIGMASKunsigned         restore_sigmask:1;
#endif
#ifdef CONFIG_MEMCGunsigned         in_user_fault:1;
#ifdef CONFIG_MEMCG_KMEMunsigned            memcg_kmem_skip_account:1;
#endif
#endif
#ifdef CONFIG_COMPAT_BRKunsigned            brk_randomized:1;
#endif
#ifdef CONFIG_CGROUPS/* disallow userland-initiated cgroup migration */unsigned         no_cgroup_migration:1;
#endif
#ifdef CONFIG_BLK_CGROUP/* to be used once the psi infrastructure lands upstream. */unsigned            use_memdelay:1;
#endifunsigned 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_group;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;u64                utime;u64               stime;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIMEu64                utimescaled;u64             stimescaled;
#endifu64               gtime;struct prev_cputime       prev_cputime;
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GENstruct vtime           vtime;
#endif#ifdef CONFIG_NO_HZ_FULLatomic_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               real_start_time;/* 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;#ifdef CONFIG_POSIX_TIMERSstruct task_cputime       cputime_expires;struct list_head        cpu_timers[3];
#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;/** executable name, excluding path.** - normally initialized setup_new_exec()* - access it with [gs]et_task_comm()* - lock it with task_lock()*/char             comm[TASK_COMM_LEN];struct nameidata        *nameidata;#ifdef CONFIG_SYSVIPCstruct sysv_sem         sysvsem;struct sysv_shm         sysvshm;
#endif
#ifdef CONFIG_DETECT_HUNG_TASKunsigned long         last_switch_count;unsigned long         last_switch_time;
#endif/* Filesystem information: */struct fs_struct     *fs;/* Open file information: */struct files_struct     *files;/* Namespaces: */struct nsproxy          *nsproxy;/* Signal handlers: */struct signal_struct     *signal;struct sighand_struct       *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;struct audit_context        *audit_context;
#ifdef CONFIG_AUDITSYSCALLkuid_t                loginuid;unsigned int           sessionid;
#endifstruct seccomp            seccomp;/* 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_TRACE_IRQFLAGSunsigned int          irq_events;unsigned long            hardirq_enable_ip;unsigned long         hardirq_disable_ip;unsigned int         hardirq_enable_event;unsigned int           hardirq_disable_event;int               hardirqs_enabled;int                hardirq_context;unsigned long           softirq_disable_ip;unsigned long            softirq_enable_ip;unsigned int          softirq_disable_event;unsigned int          softirq_enable_event;int                softirqs_enabled;int                softirq_context;
#endif#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH         48ULu64             curr_chain_key;int              lockdep_depth;unsigned int          lockdep_recursion;struct held_lock      held_locks[MAX_LOCK_DEPTH];
#endif#ifdef CONFIG_UBSANunsigned int           in_ubsan;
#endif/* Journalling filesystem info: */void                *journal_info;/* Stacked block device info: */struct bio_list           *bio_list;#ifdef CONFIG_BLOCK/* Stack plugging: */struct blk_plug           *plug;
#endif/* VM state: */struct reclaim_state       *reclaim_state;struct backing_dev_info      *backing_dev_info;struct io_context     *io_context;/* Ptrace state: */unsigned long            ptrace_message;siginfo_t            *last_siginfo;struct task_io_accounting ioac;
#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;/* Seqence number to catch updates: */seqcount_t           mems_allowed_seq;int                cpuset_mem_spread_rotor;int             cpuset_slab_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_INTEL_RDTu32              closid;u32              rmid;
#endif
#ifdef CONFIG_FUTEXstruct robust_list_head __user   *robust_list;
#ifdef CONFIG_COMPATstruct compat_robust_list_head __user *compat_robust_list;
#endifstruct list_head      pi_state_list;struct futex_pi_state     *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTSstruct perf_event_context  *perf_event_ctxp[perf_nr_task_contexts];struct mutex            perf_event_mutex;struct list_head       perf_event_list;
#endif
#ifdef CONFIG_DEBUG_PREEMPTunsigned long            preempt_disable_ip;
#endif
#ifdef CONFIG_NUMA/* Protected by alloc_lock: */struct mempolicy        *mempolicy;short                il_prev;short               pref_node_fork;
#endif
#ifdef CONFIG_NUMA_BALANCINGint             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_RSEQstruct 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;
#endifstruct tlbflush_unmap_batch   tlb_ubc;struct rcu_head         rcu;/* Cache last used pipe for splice(): */struct pipe_inode_info      *splice_pipe;struct page_frag       task_frag;#ifdef CONFIG_TASK_DELAY_ACCTstruct task_delay_info       *delays;
#endif#ifdef CONFIG_FAULT_INJECTIONint              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_LATENCYTOPint               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;#ifdef CONFIG_KASANunsigned int          kasan_depth;
#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: */struct ftrace_ret_stack      *ret_stack;/* Timestamp for last schedule: */unsigned long long     ftrace_timestamp;/** 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/* State flags for use by tracers: */unsigned long           trace;/* Bitmask and counter of trace recursion: */unsigned long            trace_recursion;
#endif /* CONFIG_TRACING */#ifdef CONFIG_KCOV/* 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;
#endif#ifdef CONFIG_MEMCGstruct mem_cgroup      *memcg_in_oom;gfp_t             memcg_oom_gfp_mask;int              memcg_oom_order;/* 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;
#endif#ifdef CONFIG_BLK_CGROUPstruct request_queue      *throttle_queue;
#endif#ifdef CONFIG_UPROBESstruct uprobe_task       *utask;
#endif
#if defined(CONFIG_BCACHE) || defined(CONFIG_BCACHE_MODULE)unsigned int         sequential_io;unsigned int          sequential_io_avg;
#endif
#ifdef CONFIG_DEBUG_ATOMIC_SLEEPunsigned long           task_state_change;
#endifint               pagefault_disabled;
#ifdef CONFIG_MMUstruct task_struct     *oom_reaper_list;
#endif
#ifdef CONFIG_VMAP_STACKstruct vm_struct        *stack_vm_area;
#endif
#ifdef CONFIG_THREAD_INFO_IN_TASK/* A live task holds one reference: */atomic_t         stack_refcount;
#endif
#ifdef CONFIG_LIVEPATCHint patch_state;
#endif
#ifdef CONFIG_SECURITY/* Used by LSM modules for access restriction: */void             *security;
#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!*/
};

task_struct结构体相对较大,在32位机器上,它大约有1.7KB。但如果考虑到该结构包含了内核管理一个进程所需要的所有信息,则它的大小也算是相当小了。进程描述符中包含的数据能完整地描述一个正在执行的程序:进程打开的文件,进程的地址空间,挂起的信号,进程的状态等。

图3-1 进程描述符和任务队列

2.1 进程描述符的分配及位置

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。在2.6以前的内核中,各个进程的task_struct存放在他们内核栈的尾端,这样做是为了让那些寄存器较少的硬件体系结构(如x86)只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。由于现在用slab分配器动态生成task struct,因此只需要在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info。

struct thread_info这个结构体是体系结构相关的。在mips架构上,其定义在文件arch/mips/include/asm/thread_info.h中,源码如下:

/** low level task data that entry.S needs immediate access to* - this struct should fit entirely inside of one cache line* - this struct shares the supervisor stack pages* - if the contents of this structure are changed, the assembly constants*   must also be changed*/
struct thread_info {struct task_struct  *task;      /* main task structure */unsigned long      flags;      /* low level flags */unsigned long      tp_value;   /* thread pointer */__u32           cpu;        /* current CPU */int            preempt_count;  /* 0 => preemptable, <0 => BUG */mm_segment_t        addr_limit; /** thread address space limit:* 0x7fffffff for user-thead* 0xffffffff for kernel-thread*/struct pt_regs        *regs;long          syscall;    /* syscall number */
};

在arm架构上,其定义在文件arch/arm/include/asm/thread_info.h中,源码如下:

/** low level task data that entry.S needs immediate access to.* __switch_to() assumes cpu_context follows immediately after cpu_domain.*/
struct thread_info {unsigned long       flags;      /* low level flags */int            preempt_count;  /* 0 => preemptable, <0 => bug */mm_segment_t        addr_limit; /* address limit */struct task_struct   *task;      /* main task structure */__u32          cpu;        /* cpu */__u32          cpu_domain; /* cpu domain */struct cpu_context_save cpu_context;    /* cpu context */__u32          syscall;    /* syscall number */__u8            used_cp[16];    /* thread used copro */unsigned long        tp_value[2];    /* TLS registers */
#ifdef CONFIG_CRUNCHstruct crunch_state crunchstate;
#endifunion fp_state        fpstate __attribute__((aligned(8)));union vfp_state     vfpstate;
#ifdef CONFIG_ARM_THUMBEEunsigned long      thumbee_state;  /* ThumbEE Handler Base register */
#endif
};

在x86架构上,其定义在文件arch/x86/include/asm/thread_info.h中,源码如下:

struct thread_info {unsigned long        flags;      /* low level flags */u32            status;     /* thread synchronous flags */
};

每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。进程描述符及内核栈如图3-2、3-3所示:

图3-2 进程描述符和内核栈1

图3-3 进程描述符和内核栈2

2.2 进程描述符的存放

内核通过一个唯一的进程标识值(process identification value)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。为了与老版本的Unix和Linux兼容,PID的最大默认值设置为32768(short int短整型的最大值),尽管这个值也可以增加到高达400万(受include/linux/threads.h中所定义的PID最大值的限制)。内核把每个进程的PID存放在其各自的进程描述符中。

这个最大值十分重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管32768对于一般的桌面系统足够用了,但是大型服务器可能需要更多进程。如果确实需要的话,可以不考虑与老式系统的兼容,由系统管理员通过修改/proc/sys/kernel/pid_max来提高上限。

在Linux内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找当前正在运行进程的进程描述符的速度就显得尤为重要了。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构来做处理。有的硬件体系结构可以拿出一个专门的寄存器来存放指向当前进程task_struct的指针,用于加快访问速度;而其他一些体系结构(如x86)由于寄存器并不富余,就只能在内核栈的尾端创建thread_info结构,通过计算偏移,间接地找到task_struct结构了(如图3-2、3-3所示)。

2.3 进程状态

进程描述符中的state域描述了进程的当前状态。系统中的每个进程必然处于五种进程状态中的一种,state域的值也必为下列五种状态标志之一:

  • TASK_RUNNING(运行)—— 进程是可执行的:它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间执行的唯一可能的状态,这种状态也可以应用到内核空间中正在执行的进程。
  • TASK_INTERRUPTIBLE(可中断)—— 进程正在睡眠,也就是说它被阻塞,等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
  • TASK_UNINTERRUPTIBLE(不可中断)—— 与TASK_INTERRUPTIBLE基本相同,只是即使接收到信号也不会被唤醒并投入运行。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断休眠状态,使用得较少。
  • __TASK_TRACED —— 被其他进程跟踪的进程。例如,通过ptrace对调试程序进行跟踪。
  • __TASK_STOPPED(停止)—— 进程停止执行。进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

进程各状态之间的转化如图3-4、3-5所示:

图3-4 进程三态模型及其状态转化

图3-5 进程五态模型及其状态转化

2.4 进程上下文

可执行程序代码是进程的重要组成部分,这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行,当一个程序调用了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中(与进程上下文相对的是中断上下文。在中断上下文中,系统不代表进程执行,而是执行一个中断处理程序)。在此上下文中,current宏是有效的。除非在此期间有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间继续执行。

系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口。

2.5 进程家族树

Unix系统的进程之间存在一个明显的继承关系,在Linux系统中也是如此。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程,最终完成系统启动的整个过程。

系统中的每个进程都必有一个父进程(init进程除外),相应地,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct、名称为parent的指针,还包含一个称为children的子进程链表。所以,对于当前进程,可以通过下面的代码获取其父进程的描述符:

struct task_struct *parent = current->parent;

同样,也可以通过以下代码依次遍历子进程:

struct task_struct *task;
struct list_head *list;list_for_each(list, &current->children) {task = list_entry(list, struct task_struct, sibling);
}

init进程的进程描述符是作为init_task静态分配的。

由于任务队列本来就是一个双向的循环链表,因此只需要通过简单的重复方式就可以遍历系统中的所有进程。对于给定的进程,通过以下代码获取链表中的下一个进程:

list_entry(task->tasks.next, struct task_struct, tasks)

获取前一个进程的方法与之相同:

list_entry(task->tasks.prev, struct task_struct, tasks)

这两个例程分别通过next_task(task)宏和prev_task(task)宏实现。

3. 进程创建

许多其他的操作系统都提供了产生spawn进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。而Unix的进程创建很特别,采用了与众不同的实现方式。它把上述步骤分解到两个单独的函数中去执行:fork()和exec()函数族。首先,fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程的值为被拷贝进程的PID)和某些资源和统计量(如挂起的信号,它没有必要被继承)。exec()函数(族)负责读取可执行文件,并将其载入地址空间开始运行。把这两个函数组合起来使用,效果跟其他操作系统使用单一的函数的效果相似。

Linux系统进程创建实质上是对父进程的拷贝,对于父进程资源的拷贝会导致进程创建效率下降,Linux采用三种机制解决此问题:

  • 写时拷贝。子进程创建时与父进程共享页框,父进程或者子进程任意一方试图改写物理页,就会产生缺页异常(Page Fault),这是内核会把这个页复制到一个新的页框中并标记为可写。原来的页面仍然是写保护的,如果有进程再次访问该页框,内核会检测该进程是否为该页框的唯一属主,如果是则将页框标记为可写。
  • 轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表、打开的文件及信号处理。
  • vfork()系统调用创建子进程时,会阻塞父进程,直到子进程退出或者execve一个新进程,父进程才恢复,这样就保证了父进程不会访问共享空间。

3.1 写时拷贝

上边已经有所提及。传统的fork()系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单并且效率低下。因为它拷贝的数据也许并不共享,更糟糕的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而是各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(上边提到的fork后立即调用exec()的情况),它们就无须复制了。

fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不用被使用的数据。由于Unix强调进程快速执行的能力,因此这个优化是很重要的。

3.2 clone、fork和vfork

Linux提供了三个创建进程的系统调用clone()、fork()和vfork()。

(1)fork:创建子进程,复制父进程的资源。

(2)vfork:创建子进程,共享父进程的资源。会将父进程阻塞,保证子进程先于父进程执行,直到子进程退出或执行新程序。

(3)clone:可以通过clone标志创建子进程,不同标识可以实现不同的效果,具体标志含义见下面clone_flag说明。

Linux4.19.90版本内核中,clone()、fork()和vfork()系统调用在kernel/fork.c中实现,源码如下:

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMUreturn _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else/* can not support in nommu mode */return -EINVAL;
#endif
}
#endif#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,0, NULL, NULL, 0);
}
#endif#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,int __user *, parent_tidptr,unsigned long, tls,int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,int __user *, parent_tidptr,int __user *, child_tidptr,unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,int, stack_size,int __user *, parent_tidptr,int __user *, child_tidptr,unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,int __user *, parent_tidptr,int __user *, child_tidptr,unsigned long, tls)
#endif
{return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif

Linux3.0版本X86架构中,clone()、fork()和vfork()系统调用源码如下:

int sys_fork(struct pt_regs *regs)
{return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}/** This is trivial, and on the face of it looks like it* could equally well be done in user mode.** Not so, for quite unobvious reasons - register pressure.* In user mode vfork() cannot have a stack frame, and if* done by calling the "clone()" system call directly, you* do not have enough call-clobbered registers to hold all* the information you need.*/
int sys_vfork(struct pt_regs *regs)
{return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,NULL, NULL);
}long
sys_clone(unsigned long clone_flags, unsigned long newsp,void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{if (!newsp)newsp = regs->sp;return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}

可以看出Linux5.6.4的clone()、fork()和vfork()都是通过_do_fork()实现,Linux3.0内核使用的是do_fork()实现。由于do_fork()存在struct pt_regs入参,所以Linux3.0内核的clone()、fork()和vfork()系统调用是和体系结构相关的。

3.3 clone_flag

clone_flag相关的宏定义在include/uapi/linux/sched.h中,源码如下(空行和中文注释是笔者为了方便阅读和理解添加的):

/** cloning flags:*/
#define CSIGNAL     0x000000ff  /* signal mask to be sent at exit *///共享内存描述符和所有页表
#define CLONE_VM    0x00000100  /* set if VM shared between processes *///共享根目录和当前目录
#define CLONE_FS    0x00000200  /* set if fs info shared between processes *///共享打开文件表
#define CLONE_FILES 0x00000400  /* set if open files shared between processes *///共享信号处理程序表、阻塞信号表和挂起信号表,必须和CLONE_VM同时使用
#define CLONE_SIGHAND   0x00000800  /* set if signal handlers and blocked signals shared *///如果父进程被跟踪,那么子进程也被跟踪
#define CLONE_PTRACE    0x00002000  /* set if we want to let tracing continue on the child too *///vfork()系统调用
#define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release *///创建出的进程是兄弟关系,不是父子关系,init进程或容器init进程不可以使用该标志,init进程是所有用户进程的祖先
#define CLONE_PARENT    0x00008000  /* set if we want to have the same parent as the cloner *///父子进程在同一个线程组,Linux内核为每一个线程和进程创建一个pid,POSIX协议规定一个进程内部线程共享一个pid,因而Linux通过线程组满足POSIX协议
#define CLONE_THREAD    0x00010000  /* Same thread group? *///clone需要自己的命名空间时设置,不能与CLONE_FS同时设置
#define CLONE_NEWNS 0x00020000  /* New mount namespace group *///共享system V SEM_UNDO操作
#define CLONE_SYSVSEM   0x00040000  /* share system V SEM_UNDO semantics *///创建新的TLS
#define CLONE_SETTLS    0x00080000  /* create a new TLS for the child *//* 把子进程的PID写入由ptid参数所指向的父进程的用户态变量
即执行如下代码:if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, args->parent_tid);
args->parent_tid为clone()系统调用入参parent_tid */
#define CLONE_PARENT_SETTID 0x00100000  /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID    0x00200000  /* clear the TID in the child */
#define CLONE_DETACHED      0x00400000  /* Unused, ignored */
#define CLONE_UNTRACED      0x00800000  /* set if the tracing process can't force CLONE_PTRACE on this clone *//* 把子进程的PID写入由ctid参数所指向的子进程的用户态变量
即执行如下代码:
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? args->child_tid : NULL;
args->child_tid 为clone()系统调用入参child_tid,p为子进程task_struct */
#define CLONE_CHILD_SETTID  0x01000000  /* set the TID in the child *//* 命名空间用于容器技术,在容器中,CPU和内存资源使通过Cgroup划分的,PID,IPC,
网络等资源使通过命名空间划分的。 */#define CLONE_NEWCGROUP      0x02000000  /* New cgroup namespace *///划分UTS(Universal Time sharing System)命名空间,分配新的UTS空间
#define CLONE_NEWUTS        0x04000000  /* New utsname namespace *///划分IPC(进程间通信)命名空间,信号量,共享内存,消息队列等进程间通信用到资源
#define CLONE_NEWIPC        0x08000000  /* New ipc namespace *///子进程创建新的User namespace,用于管理User ID和Group ID的映射,起到隔离User ID的作用。一个User namespace可以形成一个容器,容器里第一个进程uid为0
#define CLONE_NEWUSER       0x10000000  /* New user namespace *///划分PID命名空间,分配新的PID空间
#define CLONE_NEWPID        0x20000000  /* New pid namespace *///划分网络命名空间,分配网络接口
#define CLONE_NEWNET        0x40000000  /* New network namespace */#define CLONE_IO     0x80000000  /* Clone io context */

下边介绍线程的时候也会有相关内容,届时可以综合这两部分来看。

3.4 do_fork实现

从clone()、fork()和vfork()系统调用可以看出,它们最终都调用_do_fork()(老版本是do_fork())来处理。

_do_fork函数完成了创建的大部分工作,其实现在kernel/fork.c中,源码如下:

/**  Ok, this is the main fork-routine.** It copies the process, and if successful kick-starts* it and waits for it to finish using the VM if required.*/
long _do_fork(unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr,unsigned long tls)
{struct completion vfork;struct pid *pid;struct task_struct *p;int trace = 0;long nr;/** 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 ((clone_flags & CSIGNAL) != SIGCHLD)trace = PTRACE_EVENT_CLONE;elsetrace = PTRACE_EVENT_FORK;if (likely(!ptrace_event_enabled(current, trace)))trace = 0;}p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace, tls, NUMA_NO_NODE);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, parent_tidptr);if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);get_task_struct(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;
}

_do_fork首先判断是否需要trace(跟踪)这个进程,这一步主要与调试相关。

接下来调用copy_process函数,这是进程创建的关键步骤,其设置了进程描述符以及子进程所需的任何其他内核数据结构。 copy_process函数的实现在同文件中,源码较长,在此只贴出部分关键代码,其主要完成的工作如下:

(1) 检查clone_flags参数中传递的标志是否合法(是否存在冲突)。

/** 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) !=current->nsproxy->pid_ns_for_children))return ERR_PTR(-EINVAL);
}

检查项包括:

  • 不同命名空间或不同用户是否共享了根目录。如果是,返回无效参数错误(-EINVAL)。新命名空间不能和共享文件系统兼容。
  • 父子进程放入相同线程组时,是否共享了信号处理函数及被阻断的信号。如果不是,返回无效参数错误。线程必须共享信号处理程序。
  • 当共享信号处理函数及被阻断的信号时,暗含了父子进程共享地址空间(由于装载的信号处理程序只能在当前虚拟内存空间中运行,因此如果设置了共享信号处理机制就必须共享虚拟内存空间)。如果没有这一标志,返回无效参数错误。
  • 当父子进程拥有同一个父进程时,是否设置了SIGNAL_UNKILLABLE标识。如果是,返回无效参数错误。 init进程signal的flag会设置SIGNAL_UNKILLABLE标志,即init进程或者容器init进程不可以创建兄弟进程,因为这样会导致兄弟进程退出时变成僵死进程。
  • 父子进程放入相同线程组时,是否设置了不同的pid或不同的用户命名空间。如果是,返回无效参数错误。

(2)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。

p = dup_task_struct(current, node);
if (!p)goto fork_out;

dup_task_struct函数实现在 kernel/fork.c中 ,源码如下:

static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{struct task_struct *tsk;unsigned long *stack;struct vm_struct *stack_vm_area;int err;if (node == NUMA_NO_NODE)node = tsk_fork_get_node(orig);tsk = alloc_task_struct_node(node);if (!tsk)return NULL;stack = alloc_thread_stack_node(tsk, node);if (!stack)goto free_tsk;stack_vm_area = task_stack_vm_area(tsk);err = arch_dup_task_struct(tsk, orig);/** arch_dup_task_struct() clobbers the stack-related fields.  Make* sure they're properly initialized before using any stack-related* functions again.*/tsk->stack = stack;
#ifdef CONFIG_VMAP_STACKtsk->stack_vm_area = stack_vm_area;
#endif
#ifdef CONFIG_THREAD_INFO_IN_TASKatomic_set(&tsk->stack_refcount, 1);
#endifif (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;
#endifsetup_thread_stack(tsk, orig);clear_user_return_notifier(tsk);clear_tsk_need_resched(tsk);set_task_stack_end_magic(tsk);#ifdef CONFIG_STACKPROTECTORtsk->stack_canary = get_random_canary();
#endif/** One for us, one for whoever does the "release_task()" (usually* parent)*/atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACEtsk->btrace_seq = 0;
#endiftsk->splice_pipe = NULL;tsk->task_frag.page = NULL;tsk->wake_q.next = NULL;account_kernel_stack(tsk, 1);kcov_task_init(tsk);#ifdef CONFIG_FAULT_INJECTIONtsk->fail_nth = 0;
#endif#ifdef CONFIG_BLK_CGROUPtsk->throttle_queue = NULL;tsk->use_memdelay = 0;
#endif#ifdef CONFIG_MEMCGtsk->active_memcg = NULL;
#endifreturn tsk;free_stack:free_thread_stack(tsk);
free_tsk:free_task_struct(tsk);return NULL;
}

这个函数主要做了以下工作:

  • 调用alloc_task_struct_node()获得新进程的task_struct结构,并将其地址存储在tsk局部变量中。
  • 调用alloc_thread_stack_node()获得一个空闲的内存区域来存储新进程的thread_info结构。
  • 调用arch_dup_task_struct()将父进程的信息复制到tsk变量中,这是一个体系结构相关函数。
  • atomic_set(&tsk->usage, 2),usage字段表示task_struct的引用计数。

(3)初始化 ftrace ,以供内核追踪函数调用。

/** This _must_ happen before we call free_task(), i.e. before we jump* to any of the bad_fork_* labels. This is to avoid freeing* p->set_child_tid which is (ab)used as a kthread's data pointer for* kernel threads (PF_KTHREAD).*/p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;/** Clear TID on mm_release()?*/p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;ftrace_graph_init_task(p);

(4)初始化互斥锁。

rt_mutex_init_task(p);

(5)拷贝父进程的信号。

retval = copy_creds(p, clone_flags);
if (retval < 0)goto bad_fork_free;

(6)检查当前用户的线程数是否大于最大线程数。

if (nr_threads >= max_threads)goto bad_fork_cleanup_count;

(7)初始化子进程链表及兄弟进程链表。

INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);

(8)初始化自旋锁。

spin_lock_init(&p->alloc_lock);

(9)初始化挂起的信号。

init_sigpending(&p->pending);

(10)初始化子进程调度策略,优先级,调度类等进程调度相关成员 。

/* Perform scheduler related setup. Assign this task to a CPU. */
retval = sched_fork(clone_flags, p);
if (retval)goto bad_fork_cleanup_policy;

sched_fork函数在kernel/sched/core.c中实现,源码如下:

/** fork()/clone()-time setup:*/
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{unsigned long flags;__sched_fork(clone_flags, p);/** We mark the process as NEW here. This guarantees that* nobody will actually run it, and a signal or other external* event cannot wake it up and insert it on the runqueue either.*/p->state = TASK_NEW;/** Make sure we do not leak PI boosting priority to the child.*/p->prio = current->normal_prio;/** Revert to default priority/policy on fork if requested.*/if (unlikely(p->sched_reset_on_fork)) {if (task_has_dl_policy(p) || task_has_rt_policy(p)) {p->policy = SCHED_NORMAL;p->static_prio = NICE_TO_PRIO(0);p->rt_priority = 0;} else if (PRIO_TO_NICE(p->static_prio) < 0)p->static_prio = NICE_TO_PRIO(0);p->prio = p->normal_prio = __normal_prio(p);set_load_weight(p, false);/** We don't need the reset flag anymore after the fork. It has* fulfilled its duty:*/p->sched_reset_on_fork = 0;}if (dl_prio(p->prio))return -EAGAIN;else if (rt_prio(p->prio))p->sched_class = &rt_sched_class;elsep->sched_class = &fair_sched_class;init_entity_runnable_average(&p->se);/** The child is not yet in the pid-hash so no cgroup attach races,* and the cgroup is pinned to this child due to cgroup_fork()* is ran before sched_fork().** Silence PROVE_RCU.*/raw_spin_lock_irqsave(&p->pi_lock, flags);/** We're setting the CPU for the first time, we don't migrate,* so use __set_task_cpu().*/__set_task_cpu(p, smp_processor_id());if (p->sched_class->task_fork)p->sched_class->task_fork(p);raw_spin_unlock_irqrestore(&p->pi_lock, flags);#ifdef CONFIG_SCHED_INFOif (likely(sched_info_on()))memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#if defined(CONFIG_SMP)p->on_cpu = 0;
#endifinit_task_preempt_count(p);
#ifdef CONFIG_SMPplist_node_init(&p->pushable_tasks, MAX_PRIO);RB_CLEAR_NODE(&p->pushable_dl_tasks);
#endifreturn 0;
}

(11)子进程拷贝父进程的所有进程信息( files、fs、sighand、signal、mm、namespaces、io、thread_tls )。

/* 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); //复制打开的文件描述符
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;
//体系结构相关,复制task_struct->thread_struct内容
retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
if (retval)goto bad_fork_cleanup_io;

(12)分配新的pid。

if (pid != &init_struct_pid) {pid = alloc_pid(p->nsproxy->pid_ns_for_children); //在p的命名空间中分配一个新的pidif (IS_ERR(pid)) {retval = PTR_ERR(pid);goto bad_fork_cleanup_thread;}
}

(13)做一些清理工作,正常情况下,最终返回task_struct。异常情况下,返回错误值。

正常:
return p;
出错:
return ERR_PTR(retval);

copy_process函数结束并返回后,如果copy_process函数在执行过程中没有错误,则调用wake_up_new_task函数唤醒子进程并将其加入调度队列,此时状态为TASK_RUNNING。

如果指定了CLONE_VFORK标识,则要保证子进程先于父进程执行。具体方法是:把父进程加入等待队列,直到子进程释放自己的内存地址空间(子进程结束或执行新的程序)。

至此,进程创建工作就全部完成了。

4. 进程终结

当一个进程终结时,内核必须释放它所占有的资源,并把这一“不幸事件”告知其父进程。

一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用,也可能隐式地从某个程序的主函数返回(其实C语言编译器会在main()函数的返回点后面放置调用exit()的代码)。当进程接收到它既不能处理也不能忽略的信号或异常时,它还可能被动地被终结。更直观地说,linux进程退出的方式如下:

正常退出

  • 从main函数返回return
  • 调用exit
  • 调用_exit

异常退出

  • 调用abort
  • 由信号终止

此处顺带提一下_exit与exit的区别和联系:

_exit ()是linux系统调用,关闭所有文件描述符,然后退出进程;exit()是C语言的库函数,其最终调用 _exit,在此之前,先清洗标准输出的缓存,调用通过atexit注册的函数等。还有一个 _Exit,它是C语言的库函数,自c99后加入,等价于 _exit。

不管进程是怎么终结的,该任务大部分都要靠do_exit()来完成。do_exit()实现在kernel/exit.c中,源码如下:

void __noreturn do_exit(long code)
{struct task_struct *tsk = current;int group_dead;profile_task_exit(tsk);kcov_task_exit(tsk);WARN_ON(blk_needs_flush_plug(tsk));if (unlikely(in_interrupt()))panic("Aiee, killing interrupt handler!");if (unlikely(!tsk->pid))panic("Attempted to kill the idle task!");/** If do_exit is called because this processes oopsed, it's possible* that get_fs() was left as KERNEL_DS, so reset it to USER_DS before* continuing. Amongst other possible reasons, this is to prevent* mm_release()->clear_child_tid() from writing to a user-controlled* kernel address.*/set_fs(USER_DS);ptrace_event(PTRACE_EVENT_EXIT, code);validate_creds_for_do_exit(tsk);/** We're taking recursive faults here in do_exit. Safest is to just* leave this task alone and wait for reboot.*/if (unlikely(tsk->flags & PF_EXITING)) {pr_alert("Fixing recursive fault but reboot is needed!\n");/** We can do this unlocked here. The futex code uses* this flag just to verify whether the pi state* cleanup has been done or not. In the worst case it* loops once more. We pretend that the cleanup was* done as there is no way to return. Either the* OWNER_DIED bit is set by now or we push the blocked* task into the wait for ever nirwana as well.*/tsk->flags |= PF_EXITPIDONE;set_current_state(TASK_UNINTERRUPTIBLE);schedule();}exit_signals(tsk);  /* sets PF_EXITING *//** Ensure that all new tsk->pi_lock acquisitions must observe* PF_EXITING. Serializes against futex.c:attach_to_pi_owner().*/smp_mb();/** Ensure that we must observe the pi_state in exit_mm() ->* mm_release() -> exit_pi_state_list().*/raw_spin_lock_irq(&tsk->pi_lock);raw_spin_unlock_irq(&tsk->pi_lock);if (unlikely(in_atomic())) {pr_info("note: %s[%d] exited with preempt_count %d\n",current->comm, task_pid_nr(current),preempt_count());preempt_count_set(PREEMPT_ENABLED);}/* sync mm's RSS info before statistics gathering */if (tsk->mm)sync_mm_rss(tsk->mm);acct_update_integrals(tsk);group_dead = atomic_dec_and_test(&tsk->signal->live);if (group_dead) {
#ifdef CONFIG_POSIX_TIMERShrtimer_cancel(&tsk->signal->real_timer);exit_itimers(tsk->signal);
#endifif (tsk->mm)setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);}acct_collect(code, group_dead);if (group_dead)tty_audit_exit();audit_free(tsk);tsk->exit_code = code;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_FUTEXif (unlikely(current->pi_state_cache))kfree(current->pi_state_cache);
#endif/** Make sure we are holding no locks:*/debug_check_no_locks_held();/** We can do this unlocked here. The futex code uses this flag* just to verify whether the pi state cleanup has been done* or not. In the worst case it loops once more.*/tsk->flags |= PF_EXITPIDONE;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);validate_creds_for_do_exit(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();
}
EXPORT_SYMBOL_GPL(do_exit);

do_exit()做了以下繁重而复杂的工作:

(1)触发task_exit_nb通知链实例的处理函数。

profile_task_exit(tsk);

该函数在kernel/profile.c中实现,源码如下:

static BLOCKING_NOTIFIER_HEAD(task_exit_notifier);void profile_task_exit(struct task_struct *task)
{blocking_notifier_call_chain(&task_exit_notifier, 0, task);
}

触发task_exit_notifier通知,从而触发对应的处理函数。其中, BLOCKING_NOTIFIER_HEAD的定义在 include/linux/notifier.h中,源码如下:

#define BLOCKING_NOTIFIER_HEAD(name)             \struct blocking_notifier_head name =          \BLOCKING_NOTIFIER_INIT(name)#define BLOCKING_NOTIFIER_INIT(name) {             \.rwsem = __RWSEM_INITIALIZER((name).rwsem),   \.head = NULL }

__RWSEM_INITIALIZER的定义在include/linux/rwsem.h中,源码如下:

#define __RWSEM_INITIALIZER(name)                \{ __RWSEM_INIT_COUNT(name),                \.wait_list = LIST_HEAD_INIT((name).wait_list),    \.wait_lock = __RAW_SPIN_LOCK_UNLOCKED(name.wait_lock) \__RWSEM_OPT_INIT(name)             \__RWSEM_DEP_MAP_INIT(name) }

在此不再进行宏展开了,可以自行展开看一下最终的内容。

(2) 内核代码覆盖率机制相关。

kcov_task_exit(tsk);

该函数在kernel/kcov.c中实现,源码如下:

void kcov_task_init(struct task_struct *t)
{WRITE_ONCE(t->kcov_mode, KCOV_MODE_DISABLED);barrier();t->kcov_size = 0;t->kcov_area = NULL;t->kcov = NULL;
}void kcov_task_exit(struct task_struct *t)
{struct kcov *kcov;kcov = t->kcov;if (kcov == NULL)return;spin_lock(&kcov->lock);if (WARN_ON(kcov->t != t)) {spin_unlock(&kcov->lock);return;}/* Just to not leave dangling references behind. */kcov_task_init(t);kcov->t = NULL;kcov->mode = KCOV_MODE_INIT;spin_unlock(&kcov->lock);kcov_put(kcov);
}

在此也不做进一步介绍了,是一个专门的课题。

(3)检查进程的blk_plug是否为空。

WARN_ON(blk_needs_flush_plug(tsk));

blk_needs_flush_plug函数定义和实现在include/linux/blkdev.h中,源码如下:

static inline bool blk_needs_flush_plug(struct task_struct *tsk)
{struct blk_plug *plug = tsk->plug;return plug &&(!list_empty(&plug->list) ||!list_empty(&plug->mq_list) ||!list_empty(&plug->cb_list));
}

保证task_struct中的plug字段是空的,或者plug字段指向的队列是空的。plug字段的意义是stack plugging。

(4)发出OOPS消息。

if (unlikely(in_interrupt()))panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))panic("Attempted to kill the idle task!");

中断上下文不能执行do_exit函数, 也不能终止PID为0的进程。

(5)设置进程可以使用的虚拟地址的上限(用户空间)。

/** If do_exit is called because this processes oopsed, it's possible* that get_fs() was left as KERNEL_DS, so reset it to USER_DS before* continuing. Amongst other possible reasons, this is to prevent* mm_release()->clear_child_tid() from writing to a user-controlled* kernel address.*/
set_fs(USER_DS);

set_fs是一个体系结构相关的代码,其定义及实现在 arch/对应体系/include/asm/uaccess.h中 :

体系结构 定义
arm arch/arm/include/asm/uaccess.h
arm64 arch/arm64/include/asm/uaccess.h
x86 arch/x86/include/asm/uaccess.h
mips arch/mips/include/asm/uaccess.h
通用 include/asm-generic/uaccess.h

以mips为例,源码如下:

#define set_fs(x)    (current_thread_info()->addr_limit = (x))

USER_DS宏定义也在同文件中,源码如下:

/** USER_DS is a bitmask that has the bits set that may not be set in a valid* userspace address.  Note that we limit 32-bit userspace to 0x7fff8000 but* the arithmetic we're doing only works if the limit is a power of two, so* we use 0x80000000 here on 32-bit kernels.  If a process passes an invalid* address in this range it's the process's problem, not ours :-)*/#ifdef CONFIG_KVM_GUEST
#define KERNEL_DS   ((mm_segment_t) { 0x80000000UL })
#define USER_DS     ((mm_segment_t) { 0xC0000000UL })
#else
#define KERNEL_DS   ((mm_segment_t) { 0UL })
#define USER_DS     ((mm_segment_t) { __UA_LIMIT })
#endif

(6)检查并设置进程标识PF_EXITING。

/** We're taking recursive faults here in do_exit. Safest is to just* leave this task alone and wait for reboot.*/
if (unlikely(tsk->flags & PF_EXITING)) {pr_alert("Fixing recursive fault but reboot is needed!\n");/** We can do this unlocked here. The futex code uses* this flag just to verify whether the pi state* cleanup has been done or not. In the worst case it* loops once more. We pretend that the cleanup was* done as there is no way to return. Either the* OWNER_DIED bit is set by now or we push the blocked* task into the wait for ever nirwana as well.*/tsk->flags |= PF_EXITPIDONE;set_current_state(TASK_UNINTERRUPTIBLE);schedule();
}exit_signals(tsk);  /* sets PF_EXITING */

首先是检查PF_EXITING标识,此标识表示进程正在退出。如果此标识已被设置,则进一步设置PF_EXITPIDONE标识,并将进程的状态设置为不可中断状态TASK_UNINTERRUPTIBLE,并进行一次进程调度。如果此标识未被设置,则通过exit_signals来设置。

(7) 内存屏障。

/** Ensure that all new tsk->pi_lock acquisitions must observe* PF_EXITING. Serializes against futex.c:attach_to_pi_owner().*/
smp_mb();

在此补充一点内存屏障的相关知识:软件可通过读写屏障强制内存访问次序。读写屏障像一堵墙,所有在设置读写屏障之前发起的内存访问,必须先于在设置屏障之后发起的内存访问之前完成,确保内存访问按程序的顺序完成。 Linux内核提供的内存屏障API函数说明如下表。内存屏障可用于多处理器和单处理器系统,如果仅用于多处理器系统,就使用smp_xxx函数,在单处理器系统上,它们什么都不要。

内存屏障API函数说明 内存屏障的宏定义功能说明
mb() 适用于多处理器和单处理器的内存屏障
rmb() 适用于多处理器和单处理器的读内存屏障
wmb() 适用于多处理器和单处理器的写内存屏障
smp_mb() 适用于多处理器的内存屏障
smp_rmb() 适用于多处理器的读内存屏障
smp_wmb() 适用于多处理器的写内存屏障

(8)一直等待,直到获得tsk->pi_lock自旋锁。

/** Ensure that we must observe the pi_state in exit_mm() ->* mm_release() -> exit_pi_state_list().*/
raw_spin_lock_irq(&tsk->pi_lock);
raw_spin_unlock_irq(&tsk->pi_lock);

(9)如果在原子操作上下文中,打印相关信息,设置为可抢占。

if (unlikely(in_atomic())) {pr_info("note: %s[%d] exited with preempt_count %d\n",current->comm, task_pid_nr(current),preempt_count());preempt_count_set(PREEMPT_ENABLED);
}

(10)同步进程的mm的rss_stat。

/* sync mm's RSS info before statistics gathering */
if (tsk->mm)sync_mm_rss(tsk->mm);

(11)获取current->mm->rss_stat.count[member]计数。

acct_update_integrals(tsk);

acct_update_integrals函数实现在kernel/tsacct.c中,源码如下:

/*** acct_update_integrals - update mm integral fields in task_struct* @tsk: task_struct for accounting*/
void acct_update_integrals(struct task_struct *tsk)
{u64 utime, stime;unsigned long flags;local_irq_save(flags);task_cputime(tsk, &utime, &stime);__acct_update_integrals(tsk, utime, stime);local_irq_restore(flags);
}

其中,task_cputime函数获取了进程的cpu时间 。

__acct_update_integrals函数的实现在同文件中,源码如下:

static void __acct_update_integrals(struct task_struct *tsk,u64 utime, u64 stime)
{u64 time, delta;if (!likely(tsk->mm))return;time = stime + utime;delta = time - tsk->acct_timexpd;if (delta < TICK_NSEC)return;tsk->acct_timexpd = time;/** Divide by 1024 to avoid overflow, and to avoid division.* The final unit reported to userspace is Mbyte-usecs,* the rest of the math is done in xacct_add_tsk.*/tsk->acct_rss_mem1 += delta * get_mm_rss(tsk->mm) >> 10;tsk->acct_vm_mem1 += delta * tsk->mm->total_vm >> 10;
}

(12)清除定时器。

group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
#ifdef CONFIG_POSIX_TIMERShrtimer_cancel(&tsk->signal->real_timer);exit_itimers(tsk->signal);
#endifif (tsk->mm)setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}

(13)收集进程会计信息。

acct_collect(code, group_dead);

(14)审计。

if (group_dead)tty_audit_exit();
audit_free(tsk);

tty_audit_exit()记录审计事件。

audit_free(tsk)释放tsk结构体。

(15)发送pid数据并退出。

tsk->exit_code = code;
taskstats_exit(tsk, group_dead);

taskstats_exit函数实现在kernel/taskstats.c中,限于篇幅,在此不列出源码,有兴趣可以自行查看。

(16) 释放线性区描述符和页表 。

exit_mm();

(17) 输出进程会计信息 。

if (group_dead)acct_process();
trace_sched_process_exit(tsk);

(18) 释放信号量。

exit_sem(tsk);

遍历tsk->sysvsem.undo_list链表,并清除进程所涉及的每个IPC信号量的操作痕迹。

(19) 释放锁共享内存。

exit_shm(tsk);

(20) 释放文件对象相关资源。

exit_files(tsk);
exit_fs(tsk);

exit_files(tsk) 释放当前进程已经打开的文件资源 。

exit_fs(tsk)释放用于表示工作目录等结构。

(21) 脱离控制终端。

if (group_dead)disassociate_ctty(1);

(22) 释放进程命名空间。

exit_task_namespaces(tsk);

(23) 依次执行由task_work_add增加到task->task_works的函数回调 。

exit_task_work(tsk);

(24) 释放task_struct中的thread_struct结构。

exit_thread(tsk);

触发thread_notify_head链表中所有通知链实例的处理函数,用于处理struct thread_info结构体。

(25) Performance Event功能相关资源的释放。

/** 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);

(26) 分别调用调度器的dequeue_task函数迁出task,然后在调用enqueue_task加入调度组。

sched_autogroup_exit_task(tsk);

sched_autogroup_exit_task函数实现在kernel/sched/autogroup.c中,源码如下:

void sched_autogroup_exit_task(struct task_struct *p)
{/** We are going to call exit_notify() and autogroup_move_group() can't* see this thread after that: we can no longer use signal->autogroup.* See the PF_EXITING check in task_wants_autogroup().*/sched_move_task(p);
}

(27)从正在退出的任务中分离组。

cgroup_exit(tsk);

(28) 注销断点。

/** FIXME: do that only when needed, using sched_exit tracepoint*/
flush_ptrace_hw_breakpoint(tsk);

(29)同步机制。

exit_tasks_rcu_start();

exit_tasks_rcu_start函数实现在kernel/rcu/update.c中,源码如下:

/* Do the srcu_read_lock() for the above synchronize_srcu().  */
void exit_tasks_rcu_start(void)
{preempt_disable();current->rcu_tasks_idx = __srcu_read_lock(&tasks_rcu_exit_srcu);preempt_enable();
}

(30)“通知亲属,处理善后”。

exit_notify(tsk, group_dead);

exit_notify函数实现在kernel/exit.c中。源码如下:

/** Send signals to all our closest relatives so that they know* to properly mourn us..*/
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);forget_original_parent(tsk, &dead);if (group_dead)kill_orphaned_pgrp(tsk->group_leader, NULL);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;}tsk->exit_state = autoreap ? EXIT_DEAD : EXIT_ZOMBIE;if (tsk->exit_state == EXIT_DEAD)list_add(&tsk->ptrace_entry, &dead);/* mt-exec, de_thread() is waiting for group leader */if (unlikely(tsk->signal->notify_count < 0))wake_up_process(tsk->signal->group_exit_task);write_unlock_irq(&tasklist_lock);list_for_each_entry_safe(p, n, &dead, ptrace_entry) {list_del_init(&p->ptrace_entry);release_task(p);}
}

函数作用就是为当前退出的进程的子进程找到合适的父进程(进程退出,它的子进程必然成孤儿进程,需要新父进程),通常这个新的父进程是init进程。

(31) 进程事件连接器(通过它来报告进程fork、exec、exit以及进程用户ID与组ID的变化)。

proc_exit_connector(tsk);

(32) 释放struct futex_pi_state结构体所占用的内存。

if (tsk->io_context)exit_io_context(tsk);

(33) 释放与进程描述符splice_pipe字段相关的资源。

if (tsk->splice_pipe)free_pipe_info(tsk->splice_pipe);

(34)检查有多少未使用的进程内核栈。

check_stack_usage();

(35) 设置当前状态为D状态,切换进程,一去不复返。

do_task_dead();

5. 线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其它资源。线程机制支持并发程序设计技术(concurrent programming),在多处理器系统上,它也能保证真正的并行处理(parallelism)。

Linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。

上述线程机制的实现与Microsoft Windows或是Sun Solaris等操作系统的实现差异非常大。这些系统都在内核中提供了专门支持线程的机制(这些系统常常把线程称作轻量级进程(lightweight process))。"轻量级进程"这种叫法本身就概括了Linux在此处与其他系统的差异。在其他的系统中,相较于重量级的进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于Linux来说,它只是一种进程间共享资源的手段(Linux的进程本身就够轻量级了)。举个例子来说,假如我们有一个包含4个线程的进程,在提供专门线程支持的系统中,通常会有一个包含指向4个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反,Linux仅仅创建4个进程并分配4个普通的task_struct结构。建立这4个进程时指定他们共享某些资源,这是相当高雅的做法。

5.1 创建线程

线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

上面的代码产生的结果和调用fork()差不多,只是父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序。换个说法就是,新建的进程和它的父进程就是流行的所谓线程。

对比一下,一个普通的fork()的实现是:

clone(SIGCHILD, 0);

而vfork()的实现是:

clone(CLONE_VFORK | CLONE_VM | SIGCHILD, 0);

传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。表3-1列举了这些clone()用到的参数标志以及它们的作用,这些是在<linux/sched.h>中定义的。

表3-1 clone()参数标志

参数标志 含义
CLONE_FILES 父子进程共享打开的文件
CLONE_FS 父子进程共享文件系统信息
CLONE_IDLETASK 将PID设置为0(只供idle进程使用)
CLONE_NEWNS 为子进程创建新的命名空间
CLONE_PARENT 指定子进程与父进程拥有同一个父进程
CLONE_PTRACE 继续调试子进程
CLONE_SETTID 将TID回写至用户空间
CLONE_SETTLS 为子进程创建新的TLS
CLONE_SIGHAND 父子进程共享信号处理函数及被阻断的信号
CLONE_SYSVSEM 父子进程共享System V SEM_UNDO语义
CLONE_THREAD 父子进程放入相同的线程组
CLONE_VFORK 调用vfork(),所以父进程准备睡眠等待子进程将其唤醒
CLONE_UNTRACED 防止跟踪进程在子进程上强制执行CLONE_PTRACE
CLONE_STOP 以TASK_STOPPED状态开始进程
CLONE_SETTLS 为子进程创建新的TLS(thread-local storage)
CLONE_CHILD_CLEARTID 清除子进程的TID
CLONE_CHILD_SETTID 设置子进程的TID
CLONE_PARENT_SETTID 设置父进程的TID
CLONE_VM 父子进程共享地址空间

5.2 内核线程

内核经常需要在后台执行一些操作。这种任务可以通过内核线程(kernel thread)完成 —— 独立运行在内核空间的标准进程。内核线程和普通进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)。它们只是在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。

Linux确实会把一些任务交给内核线程去做,像flush和ksofirqd这些任务就是明显的例子。在装有Linux系统的机器上运行ps -ef命令,可以看到一些内核线程。这些线程在系统启动时由另外一些内核线程创建。实际上,内核线程也只能由其他内核线程创建。内核是通过从kthreadd内核进程中衍生所有新的内核线程来自动处理这一点的。在<linux/kthread.h>(路径为:include/linux/kthread.h)中申明有接口,于是,从现有内核线程中创建一个新的内核线程的方法如下:

struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),void *data,int node,const char namefmt[], ...);/*** kthread_create - create a kthread on the current node* @threadfn: the function to run in the thread* @data: data pointer for @threadfn()* @namefmt: printf-style format string for the thread name* @arg...: arguments for @namefmt.** This macro will create a kthread on the current node, leaving it in* the stopped state.  This is just a helper for kthread_create_on_node();* see the documentation there for more details.*/
#define kthread_create(threadfn, data, namefmt, arg...) \kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

新的任务是由kthread内核进程通过clone()系统调用而创建的。新的进程将运行threadfn函数,给其传递的参数为data。进程会被命名为namefmt,namefmt接受可变参数列表,类似于printf()的格式化参数。新创建的进程处于不可运行状态,如果不通过调用wake_up_process()明确地唤醒它,它不会主动运行。创建一个线程并让它运行起来,可以通过调用kthread_run()来达到。kthread_run函数在include/linux/kthread.h中定义和实现,源码如下:

/*** kthread_run - create and wake a thread.* @threadfn: the function to run until signal_pending(current).* @data: data ptr for @threadfn.* @namefmt: printf-style name for the thread.** Description: Convenient wrapper for kthread_create() followed by* wake_up_process().  Returns the kthread or ERR_PTR(-ENOMEM).*/
#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;                                   \
})

这个例程是以宏实现的,只是简单地调用了kthread_create()和wake_up_process()。

内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的task_struct结构的地址。kthread_stop()函数的实现在kernel/kthread.c中,源码如下:

/*** kthread_stop - stop a thread created by kthread_create().* @k: thread created by kthread_create().** Sets kthread_should_stop() for @k to return true, wakes it, and* waits for it to exit. This can also be called after kthread_create()* instead of calling wake_up_process(): the thread will exit without* calling threadfn().** If threadfn() may call do_exit() itself, the caller must ensure* task_struct can't go away.** Returns the result of threadfn(), or %-EINTR if wake_up_process()* was never called.*/
int kthread_stop(struct task_struct *k)
{struct kthread *kthread;int ret;trace_sched_kthread_stop(k);get_task_struct(k);kthread = to_kthread(k);set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);kthread_unpark(k);wake_up_process(k);wait_for_completion(&kthread->exited);ret = k->exit_code;put_task_struct(k);trace_sched_kthread_stop_ret(ret);return ret;
}
EXPORT_SYMBOL(kthread_stop);

四. 技术应用

本章既然名为”技术应用“,那么介绍一些在应用层进行进程创建和退出的代码最为合适和贴切。

例程1 —— 进程的创建和进程资源的回收

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>int main(int argc, char *argv[])
{pid_t pid;int a = 0;//创建子进程if(-1 == (pid = fork())){perror("fork failed");return -1;}//子进程if(0 == pid){a = 1;printf("this is the child process: %d\n", getpid());printf("in the child process, a is %d\n", a);}//父进程else if(pid > 0){printf("this is the parent process: %d\n", getpid());printf("in the parent process, a is %d\n", a);}return 0;
}

解释说明:

在子进程退出后,父进程会回收子进程的资源。使用wait、waitpid系统调用回收子进程的资源。 使用wait、waitpid系统调用回收子进程的资源。 如果父进程早于子进程结束,那么此子进程的父亲就改变为init进程,这种进程被成为孤儿进程。

如上述函数这样的写法,有时可能是父进程先返回,也就意味着父进程先结束;有时可能是子进程先返回,也就意味着子进程先结束。看似很简单,无非就是谁先谁后而已,但是实际上两种机制差别很大(有点像继承法~)。对于第一种情况,机制如上边一段所述;对于后一种情况,子进程会在某一极短时期成为僵尸进程,具体见下面例程4。

例程2 —— wait

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main(int argc, char **argv)
{pid_t pid;int s;//创建子进程pid = fork();if(pid == -1){perror("fork failed");return -1;}//子进程if(pid == 0){printf("this is the child process: %d\n", getpid());getchar();exit(0);}//父进程else{//等待子进程的结束wait(&s);if(WIFEXITED(s)){//子进程正常终止printf("status:%d\n", WEXITSTATUS(s));}//检测子进程是否被信号终止if(WIFSIGNALED(s)){//输出终止子进程的信号编号printf("signum :%d\n", WTERMSIG(s));}printf("parent exit\n");}return 0;
}

函数说明:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

功能:等待进程改变状态。

参数:
status:退出状态码的地址。子进程的退出状态存放在这块地址空间里。可以使用一些宏检测退出原因。
WIFEXITED(status):如果正常死亡,返回真。
WEXITSTATUS(status):返回子进程的退出状态和0377的与,那个值。
WIFSIGNALED(status):如果子进程被信号终止,返回真。
WTERMSIG(status):检测被几号信号终止。只有上个宏为真的时候,才使用。

返回值:
-1:错误
返回终止的子进程的pid。

例程3 —— waitpid

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main(int argc, char *argv)
{pid_t pid;int s;//创建子进程pid = fork();if(pid == -1){perror("fork failed");return -1;}//子进程if(pid == 0){printf("this is the child process: %d\n", getpid());getchar();exit(0);}//父进程else{//非阻塞等待子进程的结束waitpid(-1, &s, WNOHANG);if(WIFEXITED(s)){//子进程正常终止printf("status:%d\n", WEXITSTATUS(s));}//检测子进程是否被信号终止if(WIFSIGNALED(s)){//输出终止子进程的信号编号printf("signum :%d\n", WTERMSIG(s));}printf("parent exit\n");}return 0;
}

函数说明:

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);

功能:等待进程改变状态。

参数:
pid:
< -1: pid取绝对值,如果子进程的组id等于这个绝对值,那么这个子进程就被等待。
-1:等待任意子进程。
0:等待和当前进程有同一个组id的子进程。
> 0:等待子进程的pid是pid参数的子进程。
status:同wait参数的使用。
options:
WNOHANG:非阻塞回收。
0:阻塞回收。

返回值:
-1:错误。
0:没有子进程退出。
回收的子进程的pid。

例程4 —— 僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main(int argc, char *argv)
{pid_t pid;//创建子进程pid = fork();if(pid == -1){perror("fork failed");return -1;}//子进程if(pid == 0){printf("this is the child process: %d\n", getpid());exit(0);}//父进程else{sleep(20);wait(NULL);}return 0;
}

解释说明:

子进程已经终止,但是父进程还没有回收子进程的资源,这时候的子进程处于僵尸状态,成为僵尸进程。

例程5 —— 线程创建和退出

#include <stdio.h>
#include <pthread.h> //线程头文件//pthread不是linux下的默认的库,也就是在链接的时候,无法找到phread库中各函数的入口地址,于是链接会失败
//在gcc编译的时候,附加要加 -lpthread参数即可解决。gcc -o run pthreadtext.c -lpthreadint a = 0;void *myThread1(void) //线程函数
{int i;for(i=0; i<5; i++){a++;printf("this is the 1st thread\n");}
}void *myThread2(void)
{int i;for(i=0; i<5; i++){printf("this is the 2nd thread\n");sleep(1);}printf("a is %d\n", a);
}int main(int argc, char *argv[])
{int i = 0;int ret = 0;pthread_t id1, id2;ret = pthread_create(&id1, NULL, (void*)myThread1, NULL); //创建线程1if(ret){printf("thtrea1 create error\n");return 1;}ret = pthread_create(&id2, NULL, (void*)myThread2, NULL); //创建线程2if(ret){printf("thread2 create error\n");return 1;}pthread_join(id1, NULL); //当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行pthread_join(id2, NULL);return 0;
}

解释说明:

初始时全局变量a的值为0,此后主函数依次创建了线程1和线程2。由于线程1有休眠,而线程2没有休眠,因此线程1先执行完,a自增了5次,最终结果是5。最后线程2结束休眠退出时,打印a的值为5。这就验证了线程之间共享地址空间的情况。

五、小结

本文首先论述了写作背景及成文缘由。接下来,详细讲述了操作系统中的核心概念——进程(Linux系统)。在介绍了进程相关概念后,讲解了内核代码中进程描述符的结构并重点讲述了进程的状态,提及了进程上下文和家族树等概念。随后来到了本文的核心部分:进程创建、进程终结的源代码解析。诚然,这部分对于初学者乃至刚入门的技术人员来说难度较高且过于枯燥了,读来也许会无法坚持或昏昏欲睡。但笔者在此仍然坚持贴出较为详尽的代码的原因暨初衷有以下几点:(1)对于那些渴望精通Linux内核的人来说,这点耐心还是要有的。虽然代码比较多,但是其实也不过是只展开到往下一二层而已,大多数代码还都没有展开讲,有的甚至没有讲解;(2)本文是综合了网上诸篇相关文章之所长写出来的,虽然前一个理由中提到了只是“蜻蜓点水”,但对于其它一些只贴出源码而不深入讲解的文章来说,也算是“鸿篇巨制”了。笔者希望读者读完这篇文章后,就已经可以对于进程创建、进程退出的整个机制“成竹在胸”了,无需再去网上东看一段代码、西读一篇文章。在进程创建及退出的源码解析之后,进行了线程以及内核线程相关知识的讲解。随后在技术应用部分,给出了比较简单和常用的应用层创建、退出进程(包括线程)的例程,读者可以根据这些程序代码,从应用层的角度理解一下内核。

在这里必须说明,本文虽然名为Linux内核进程管理,但是实际上并没有包含进程调度的相关内容,因为进程调度同样是一个很大的概念,其中涉及的知识点非常之多,适合于单独写成一篇文章。关于进程调度的相关知识,笔者会单独撰写一篇《Linux内核进程调度专题报告》与读者分享。

六、参考资料

[1] (美)Love R.Linux内核设计与实现(第3版)[M].陈莉君,康华,译.北京:机械工业出版社,2012 :20-34.

[2] Jiang M. Linux 进程管理[EB/OL].Linux 进程管理_Mike江的博客-CSDN博客_linux进程管理, 2015-04-21.

[3] Lu A. Linux进程管理(1)进程的诞生[EB/OL]. Linux进程管理 (1)进程的诞生 - ArnoldLu - 博客园, 2018-04-17.

[4] qq_43313035. Linux内核do_fork()分析 [EB / OL]. Linux内核do_fork()分析_qq_43313035的博客-CSDN博客, 2019-05-24.

[5] 杨博东. Linux内核 fork 源码分析[EB/OL]. Linux内核 fork 源码分析_杨博东的博客的博客-CSDN博客, 2017-11-27.

[6] 迷途小生. 深入Linux内核(进程篇)—进程创建与退出[EB / OL]. 深入Linux内核(进程篇)—进程创建与退出_迷途小生的博客-CSDN博客_copy_thread_tls, 2020-07-09.

[7] lonewolfxw. linux fork系统调用[EB / OL]. linux fork系统调用 - it610.com, 2012-09-17.

[8] yooooooo. Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)[EB / OL]. Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四) - yooooooo - 博客园, 2018-09-17.

[9] BugMan. Linux内核之exit函数[EB / OL]. Linux内核之exit函数-BugMan-ChinaUnix博客, 2019-10-12.

Linux内核进程管理专题报告相关推荐

  1. Linux内核进程管理基本概念-进程、运行队列、等待队列、进程切换、进程调度

    下面简述一些基本概念,以及对内核代码做最初步的了解: 一 Linux内核进程管理基础 Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到 ...

  2. linux内存管理实验malloc,linux内存管理实验报告.doc

    linux内存管理实验报告 操作系统实验报告 院别:XXXXXX 班级:XXXXXX 学号:XXXXXX 姓名:稻草人 实验题目:内存管理实验 实验目的 通过本次试验体会操作系统中内存的分配模式: 掌 ...

  3. Linux系统内存管理实验报告,linux内存管理实验报告

    <linux内存管理实验报告>由会员分享,可在线阅读,更多相关<linux内存管理实验报告(13页珍藏版)>请在人人文库网上搜索. 1.操作系统实验报告院别:XXXXXX班级: ...

  4. 陈老师Linux内核进程管理导学

    <Linux内核分析与应用>第三章 : 进程管理 你认识进程么,就相当于问你认识自己一样难于回答,因为进程每一瞬间都是变化的,就像你的思想无时无刻不在变化一样,因此,本章对进程的讲解可以说 ...

  5. Linux内核进程管理:进程的“内核栈”、current宏、进程描述符

    目录 linux 进程内核栈 概念 thread_info 有什么用? thread_info .内核栈.task_struct 关联 current 宏 1.arm 2.ARM64 3.x86 SY ...

  6. Linux进程管理专题

    Linux进程管理 (1)进程的诞生介绍了如何表示进程?进程的生命周期.进程的创建等等? Linux支持多种调度器(deadline/realtime/cfs/idle),其中CFS调度器最常见.Li ...

  7. Linux 内核进程管理之进程ID

    Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一.该数据结构 ...

  8. Linux内核进程管理实时调度与SMP

    一,实时调度器类 实时调度类有两类进程: 循环进程SCHED_RR:循环进程有时间片,随着进程的运行时间会减少.当时间片用完时又将其置为初值,并将进程置于队列末尾.先进先出SCHED_FIFO:没有时 ...

  9. linux - 进程管理

    引用 Linux进程管理专题 Linux进程管理与调度-之-目录导航 Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五) 蜗窝科技-进程管 ...

最新文章

  1. 利用多项式特征生成与递归特征消除解决特征组合与特征选择问题
  2. python实验报告二_分组级运算和转换
  3. 知乎改版api接口之scrapy自动登陆
  4. OnClientClick和OnClick同时使用!
  5. d3.js 实现烟花鲜果
  6. 计算机打字比赛活动策划书怎么写,打字比赛策划书范文.docx
  7. Spring事务传播行为7种类型 --- 看一遍就能记住!
  8. 基于Python的语音识别控制系统
  9. Kali Linux 暴力破解wifi密码详细步骤
  10. html+游戏转盘,javascript+HTML5 Canvas绘制转盘抽奖
  11. CumtCTF第二次双月赛Writeup(Web详解)
  12. linux下的流量监控之应用程序防火墙
  13. 美创科技入选第九届CNCERT网络安全应急服务支撑单位
  14. 转载:机器学习算法工程师秋招总结
  15. photoshop 图片裁剪与填充前景色及背景色
  16. rk3568和rk3399性能对比 rk3568和rk3399区别
  17. mysql 高手_求mysql高手
  18. 将时间戳“年月日 时分秒”格式转换成“年月日”格式
  19. 区块链社交APP协议分析:Qbao
  20. Java基础之-内部类(成员内部类,静态内部类,局部内部类,匿名内部类)

热门文章

  1. golang---并发Goroutine
  2. U盘数据丢失怎么办?U盘数据丢失恢复方法?
  3. xlslib生成excel文件
  4. tomcat设置UTF-8编码
  5. 客观评价下软件培训机构
  6. 【颜纠日记】你还不会用百度搜索吗?搜索引擎关键词技巧宝典。
  7. USB UDC驱动 gadget驱动
  8. w ndoWs8pE模式下载,u启动windows8PE系统维护工具箱下载_u启动windows8PE系统维护工具箱官方下载-太平洋下载中心...
  9. Brup_Suite安装配置及基础使用----最详细的教程(测试木头人)
  10. SIP系统封装技术浅析