回顾下操作系统概念:现代计算机往往都是“同时”运行多个任务。系统若只有一个处理器,那么给定时刻只可能有一个任务在执行。而操作系统通过进程管理和调度,切换正在执行的任务,是用户在感官上认为计算机是并行执行多个任务。当然,若是多处理器系统,真正同时执行的任务可以达到处理器的数目。
内核进行进程管理的主要解决的问题:
- 任务有轻重缓急之分,需要可以根据任务的紧急程度给予任务不同的执行优先级和时间。同时尽可能保证任务执行的公平性。
进程状态
在Linux系统中,我们所讲的任务即进程。进程调度,就是根据当前系统的运行状况,对进程状态的切换。
Linux中进程主要有如下状态:
- 运行:该进程此刻正在执行。
- 等待:进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次任务切换时选择该进程。
- 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件(或某种资源)。调度器无法在下一次任务切换时选择该进程。
以上状态可以相互转换(等待–>睡眠转换除外),转换的条件主要有:
- 进程时间片用完或轮转到该进程
- 进程阻塞等待某种资源/某种资源准备好了
进程表示
进程用task_struct结构体来表示,定义在include/linux/sched.h如下(省略部分成员)。重点结构体成员意义注释在代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
| struct task_struct { #ifdef CONFIG_THREAD_INFO_IN_TASK struct thread_info thread_info; #endif
volatile long state; void *stack; atomic_t usage; unsigned int flags; unsigned int ptrace; int on_rq; int prio, static_prio, normal_prio; unsigned int rt_priority; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; #ifdef CONFIG_CGROUP_SCHED struct task_group *sched_task_group; #endif struct sched_dl_entity dl; #ifdef CONFIG_PREEMPT_NOTIFIERS struct hlist_head preempt_notifiers; #endif unsigned int policy; int nr_cpus_allowed; cpumask_t cpus_allowed;
struct list_head tasks; struct mm_struct *mm, *active_mm; u64 vmacache_seqnum; struct vm_area_struct *vmacache[VMACACHE_SIZE]; int exit_state; int exit_code, exit_signal; unsigned sched_reset_on_fork:1; unsigned sched_contributes_to_load:1; unsigned sched_migrated:1; unsigned sched_remote_wakeup:1; unsigned :0; unsigned in_execve:1; unsigned in_iowait:1; #if !defined(TIF_RESTORE_SIGMASK) unsigned restore_sigmask:1; #endif #ifdef CONFIG_MEMCG unsigned memcg_may_oom:1; #ifndef CONFIG_SLOB unsigned memcg_kmem_skip_account:1; #endif #endif #ifdef CONFIG_COMPAT_BRK unsigned brk_randomized:1; #endif #ifdef CONFIG_CGROUPS struct restart_block restart_block; pid_t pid; pid_t tgid;
struct task_struct __rcu *real_parent; struct task_struct __rcu *parent; struct list_head children; struct list_head sibling; struct task_struct *group_leader; struct list_head ptraced; struct list_head ptrace_entry;
struct pid_link pids[PIDTYPE_MAX]; struct list_head thread_group; struct list_head thread_node; struct completion *vfork_done; int __user *set_child_tid; int __user *clear_child_tid;
cputime_t utime, stime, utimescaled, stimescaled; cputime_t gtime; struct prev_cputime prev_cputime; #ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN seqcount_t vtime_seqcount; unsigned long long vtime_snap; enum { VTIME_INACTIVE = 0, VTIME_USER, VTIME_SYS, } vtime_snap_whence; #endif
#ifdef CONFIG_NO_HZ_FULL atomic_t tick_dep_mask; #endif unsigned long nvcsw, nivcsw; u64 start_time; u64 real_start_time; unsigned long min_flt, maj_flt; struct task_cputime cputime_expires; struct list_head cpu_timers[3];
const struct cred __rcu *ptracer_cred; const struct cred __rcu *real_cred; const struct cred __rcu *cred; 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; #endif struct fs_struct *fs; struct files_struct *files; struct nsproxy *nsproxy; struct signal_struct *signal; struct sighand_struct *sighand;
sigset_t blocked, real_blocked; sigset_t saved_sigmask; struct sigpending pending;
unsigned long sas_ss_sp; size_t sas_ss_size; unsigned sas_ss_flags;
struct callback_head *task_works;
struct audit_context *audit_context; #ifdef CONFIG_AUDITSYSCALL kuid_t loginuid; unsigned int sessionid; #endif struct seccomp seccomp;
u32 parent_exec_id; u32 self_exec_id;
spinlock_t alloc_lock; raw_spinlock_t pi_lock; struct wake_q_node wake_q;
struct reclaim_state *reclaim_state; struct backing_dev_info *backing_dev_info; struct io_context *io_context; unsigned long ptrace_message; siginfo_t *last_siginfo; struct task_io_accounting ioac; #if defined(CONFIG_TASK_XACCT) u64 acct_rss_mem1; u64 acct_vm_mem1; cputime_t acct_timexpd; #endif #ifdef CONFIG_CPUSETS nodemask_t mems_allowed; seqcount_t mems_allowed_seq; int cpuset_mem_spread_rotor; int cpuset_slab_spread_rotor; #endif #ifdef CONFIG_CGROUPS struct css_set __rcu *cgroups; struct list_head cg_list; #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; #endif #ifdef CONFIG_PERF_EVENTS struct 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_PREEMPT unsigned long preempt_disable_ip; #endif struct thread_struct thread; ........ };
|
因为要支持各种各样的功能,task_struct已经变得非常大。不过总体上,结构体可以被划分为如下部分:
- 状态和执行信息,如待决信号、使用的二进制格式(和其他系统二进制格式的任何仿真信息)、进程ID号( pid)、到父进程及其他有关进程的指针、优先级和程序执行有关的时间信息(例如CPU时间)。
- 有关已经分配的虚拟内存的信息。
- 进程身份凭据,如用户ID、组ID以及权限①等。可使用系统调用查询(或修改)这些数据。
- 使用的文件包含程序代码的二进制文件,以及进程所处理的所有文件的文件系统信息,这些都必须保存下来。
- 进程信息记录该进程特定于CPU的运行时间数据(该结构的其余字段与所使用的硬件无关)。
- 在与其他应用程序协作时所需的进程间通信有关的信息。
- 该进程所用的信号处理程序,用于响应到来的信号。
进程ID号
在task_struct结构体里,我们看到了很多进程ID相关字段,初看会很容易混淆。本节介绍Linux进程ID管理相关思想,帮助理解这些字段的含义。
乍一看进程ID管理应该比较简单:内核只需要保证分配的id不唯一,释放掉的id可以被其他新创建的进程id使用即可。但是事实并非如此,内核需要做如下考量:
- 内核有命名空间的概念,一个进程可以出现在多个命名空间,它在不同的命名空间的id是不同的。
- 同一个进程可以有多个线程,这些线程(其实也是task_struct)共享同一个线程组id (TGID)
- 进程可以合并为进程组,而进程组又可以合并为会话(Session)。组或者会话里的进程共享相同的组id或会话id。
PID分配需要在特定的命名空间内保证id的唯一性。用来表示PID的命名空间定义为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| struct pid_namespace { struct kref kref; struct idr idr; struct rcu_head rcu; unsigned int pid_allocated; struct task_struct *child_reaper; struct kmem_cache *pid_cachep; unsigned int level; struct pid_namespace *parent; #ifdef CONFIG_PROC_FS struct vfsmount *proc_mnt; struct dentry *proc_self; struct dentry *proc_thread_self; #endif #ifdef CONFIG_BSD_PROCESS_ACCT struct fs_pin *bacct; #endif struct user_namespace *user_ns; struct ucounts *ucounts; struct work_struct proc_work; kgid_t pid_gid; int hide_pid; int reboot; struct ns_common ns; } __randomize_layout
|
而内核管理命名空间内的pid也主要围绕两个数据结构:
1 2 3 4 5 6 7 8 9 10 11 12
| struct upid { int nr; struct pid_namespace *ns; }; struct pid { atomic_t count; unsigned int level; struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; };
|
其中PIDTYPE_MAX为pid类别的枚举类型最大值,具体该枚举类型定义如下:
1 2 3 4 5 6 7 8
| enum pid_type { PIDTYPE_PID, PIDTYPE_TGID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX, };
|
除此之外,task_struct还保留了两个pid,分别为:
- pid: 初使命名空间(即init进程所在空间)中该进程的全局ID号
- tgid:初使命名空间中该进程的线程组ID号,若该进程非多线程进程,则值与pid相同。
一张图表示task_struct中进程id的相互关联:
pid数据结构关系图(引用自《深入理解linux内核架构》)
注意,上图结构为2.6版内核中数据结构。新版内核(截至目前应该是5.)对结构会有部分调整,但总体管理方式和数据结构间关联未变。
进程间关系
进程可以有子进程,其子进程链表用task_struct的children元素表示。一个子进程和父进程的其他子进程互为兄弟进程,通过task_struct的sibling元素相互关联。
总结
本文介绍了linux内核管理的基本概念,以及相应的数据结构。后续介绍会包含进程调度基本架构和思想。