概述

现代操作系统都是可以同时运行多个任务的,这里的任务我们称之为进程,可以划分的更细一点:即有线程(轻量级的进程),在操作系统的层面上 进程和线程除了在内存空间上的区别之外并未其他区别,他们都是使用task_struct来进行描述的。既然 同时运行多个进程,那么必然涉及的就是进程的管理、调度以及生命周期,最近查阅了相关的资料,姑且记之。

详解

基本概念

进程是正在运行的程序,需要一些资源来完成自己的任务,其中包含了几个容易混淆的概念:

  • 进程和程序: 程序通常成为text段(在内存管理中有说明,就是进程的一块内存空间),进程除了包含text段外,还需要 程序计数器、cpu寄存器、堆、栈、数据段等用来存放运行时信息。

  • 进程和线程: 在linux下,线程也叫做轻量级进程,也需要一个task_struct来描述,两者的区别是线程没有自己独立的内存空间 ,线程是共享父进程的内存空间的。这里我们提到的task_struct就是描述进程的结构体

如下一张图来展示一下进程的内存空间分布:

上面是进程的资源的分布,不过这些资源需要一个结构来进行管理,管理这块内存空间的结构我们称之为进程控制块-也即是pcb(也可以叫做进程管理器), pcb对应的数据结构就是上面的task_struct。进程控制块的主要作用如下:

  • 实现cpu的调度
  • 在一定的策略下分配资源给进程(这里的资源应该是包含cpu的,上面只是调度)
  • 实现进程同步和进程间的通讯
  • 实现避免死锁策略和保护机制

进程创建和结束

进程的状态

上面介绍了进程的一些基本概念,接下来我们来看一下进程的生命周期,在进入正题之前我们先看一下进程的状态(这里的进程的状态指用户进程的状态,并非内核进程的状态,或者通俗点说就是用户态进程的状态,而非内核态)。

  • task_running:进程出于运行(系统当前的进程)或者准备运行状态(等待系统将cpu分配给它,已经具备了除cpu之外的所有的资源,不会因为其他的资源而陷入等待)
  • task_interruptable:等待某个条件如中断、信号、socket、信号量等资源(等待状态)
  • task_uninterruptable:进程处于睡眠状态,信号无法唤醒,只有wakeup才可以唤醒,这种情况不是太好理解,需要结合上面 提到的是用户进程而非内核进程来进行理解,用户进程出于睡眠状态,比如读写外设的过程中,进程陷入内核态,用户进程在这个状态的时候就不可以 被打断,否则可能出现意想不到的问题
  • task_stopped: 进程停止,进程收到响应的信号的时候就会进入该状态
  • task_traced: 进程执行被调试器停止,调试程序使用ptrace系统调用来监控程序,没有研究过
  • exit_zombie: 俗称的墓碑状态,该进程已经结束,但是父进程还没有调用wait4或者waitpid返回当前进程的信息,此时进程描述符(task_struct)仍然存在
  • exit_dead:进程的最终状态,父进程调用wait4或者waitpid后,系统正在删除该进程,对应的进程描述符也会被销毁

状态转换过程中的异常

接下来说一下进程状态转换的时候会出现的一些异常:

  • 僵尸进程:当进程在父进程还没有调用wait4或者waitpid的时候就退出了,那么该进程对应的进程描述符就没有办法销毁, 引发的最终的问题就是内存的泄漏,不过进程描述符本身消耗的内存也不是太大。这种情况下只可以通过重启机器来重制这些进程
  • 孤儿进程:当某个父进程退出的时候其子进程还在执行,这种情况下子进程会被init进程收养

进程状态转换示意图

最后来用一张图来描述一下进程状态转换:

描述进程的结构体

上面介绍了进程的状态之后,还需要介绍一下进程描述符,毕竟进程的创建就是生成这个进程描述符,每个进程的描述符包含了进程 状态、地址空间、打开的文件、进程优先级等信息,如下我们看一下结构体:

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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;

int lock_depth; /* BKL lock depth */

#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu;
#endif
#endif

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_PREEMPT_NOTIFIERS
/* list of struct preempt_notifier: */
struct hlist_head preempt_notifiers;
#endif

/*
* fpu_counter contains the number of consecutive context switches
* that the FPU is used. If this is over a threshold, the lazy fpu
* saving becomes unlazy to save the trap. This is an unsigned char
* so that after 256 times the counter wraps and the behavior turns
* lazy again; this to deal with bursty apps that only use FPU for
* a short time
*/
unsigned char fpu_counter;
#ifdef CONFIG_BLK_DEV_IO_TRACE
unsigned int btrace_seq;
#endif

unsigned int policy;
cpumask_t cpus_allowed;

#ifdef CONFIG_TREE_PREEMPT_RCU
int rcu_read_lock_nesting;
char rcu_read_unlock_special;
struct rcu_node *rcu_blocked_node;
struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
struct sched_info sched_info;
#endif

struct list_head tasks;
struct plist_node pushable_tasks;

struct mm_struct *mm, *active_mm;

/* task state */
int exit_state;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
unsigned int personality;
unsigned did_exec:1;
unsigned in_execve:1; /* Tell the LSMs that the process is doing an
* execve */
unsigned in_iowait:1;


/* Revert to default priority/policy when forking */
unsigned sched_reset_on_fork:1;

pid_t pid;
pid_t tgid;

#ifdef CONFIG_CC_STACKPROTECTOR
/* Canary value for the -fstack-protector gcc feature */
unsigned long stack_canary;
#endif

/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */

/*
* ptraced is the list of tasks this task is using ptrace on.
* This includes both natural children and PTRACE_ATTACH targets.
* p->ptrace_entry is p's link on the p->parent->ptraced list.
*/
struct list_head ptraced;
struct list_head ptrace_entry;

/*
* This is the tracer handle for the ptrace BTS extension.
* This field actually belongs to the ptracer task.
*/
struct bts_context *bts;

/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;

struct completion *vfork_done; /* for vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */

cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
cputime_t prev_utime, prev_stime;
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time; /* monotonic time */
struct timespec real_start_time; /* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt;

struct task_cputime cputime_expires;
struct list_head cpu_timers[3];

/* process credentials */
const struct cred *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred *cred; /* effective (overridable) subjective task
* credentials (COW) */
struct mutex cred_guard_mutex; /* guard against foreign influences on
* credential calculations
* (notably. ptrace) */
struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */

char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by flush_old_exec */
/* file system info */
int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
struct sysv_sem sysvsem;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
unsigned long last_switch_count;
#endif
/* CPU-specific state of this task */
struct thread_struct thread;
/* 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, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;

unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
uid_t loginuid;
unsigned int sessionid;
#endif
seccomp_t seccomp;

/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
* mempolicy */
spinlock_t alloc_lock;

#ifdef CONFIG_GENERIC_HARDIRQS
/* IRQ handler threads */
struct irqaction *irqaction;
#endif

/* Protection of the PI data structures: */
spinlock_t pi_lock;

#ifdef CONFIG_RT_MUTEXES
/* PI waiters blocked on a rt_mutex held by this task */
struct plist_head pi_waiters;
/* 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_IRQFLAGS
unsigned int irq_events;
int hardirqs_enabled;
unsigned long hardirq_enable_ip;
unsigned int hardirq_enable_event;
unsigned long hardirq_disable_ip;
unsigned int hardirq_disable_event;
int softirqs_enabled;
unsigned long softirq_disable_ip;
unsigned int softirq_disable_event;
unsigned long softirq_enable_ip;
unsigned int softirq_enable_event;
int hardirq_context;
int softirq_context;
#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];
gfp_t lockdep_reclaim_gfp;
#endif

/* journalling filesystem info */
void *journal_info;

/* stacked block device info */
struct bio *bio_list, **bio_tail;

/* VM state */
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; /* For ptrace use. */
struct task_io_accounting ioac;
#if defined(CONFIG_TASK_XACCT)
u64 acct_rss_mem1; /* accumulated rss usage */
u64 acct_vm_mem1; /* accumulated virtual memory usage */
cputime_t acct_timexpd; /* stime + utime since last update */
#endif
#ifdef CONFIG_CPUSETS
nodemask_t mems_allowed; /* Protected by alloc_lock */
int cpuset_mem_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS
/* Control Group info protected by css_set_lock */
struct css_set *cgroups;
/* cg_list protected by css_set_lock and tsk->alloc_lock */
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;
struct mutex perf_event_mutex;
struct list_head perf_event_list;
#endif
#ifdef CONFIG_NUMA
struct mempolicy *mempolicy; /* Protected by alloc_lock */
short il_next;
#endif
atomic_t fs_excl; /* holding fs exclusive resources */
struct rcu_head rcu;

/*
* cache last used pipe for splice
*/
struct pipe_inode_info *splice_pipe;
#ifdef CONFIG_TASK_DELAY_ACCT
struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
int make_it_fail;
#endif
struct prop_local_single dirties;
#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.
*/
unsigned long timer_slack_ns;
unsigned long default_timer_slack_ns;

struct list_head *scm_work_list;
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
/* Index of current stored adress in ret_stack */
int curr_ret_stack;
/* Stack of return addresses for return function tracing */
struct ftrace_ret_stack *ret_stack;
/* time stamp 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 for the tracing */
atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING
/* state flags for use by tracers */
unsigned long trace;
/* bitmask of trace recursion */
unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
unsigned long stack_start;
};

有点长,这也是从其他地方拷贝过来的。里面大致包含了以下几种信息:

  • 标识符:描述本进程的唯一标识符,用来区别其他进程
  • 状态:任务状态,推出代码,退出信号等
  • 优先级:相对于其他进程的优先级
  • 程序计数器:程序中即将被执行的下一条指令的地址
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据:进程执行时处理器的寄存器中的数据
  • I/O状态信息:包括显示的I/O请求,分配的进程I/O设备和进程使用的文件列表
  • 记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记帐号等

进程结构体内存分布

由于下文会涉及进程调度相关的内容,因此这里强调以下,task_struct中同样包含了进程队列指针, 所有进程均有各自的PCB。且各个PCB会串在一起,形成一个双向链表。其next_task和prev_task就表示上一个或下一个PCB,即前后指针。进程链表的头和尾都是0号进程。 对应的指针如next_task,prev_task,当然还有更多的指针,接下来我们用一张图来演示一下这个结构:

进程对应的结构体如上所示,这种结构体被thread_info所指向,下图描述了thread_info、进程内核栈、task_struct之间的关系:

这里需要说明一下,内核会给每个进程分配8k的内存,用来存储thread_info结构和内核栈,内核栈是由于进程 在内核态运行的时候需要自己的堆栈来保存一些变量,这个栈不同于用户态进程所用的栈。 另外thread_info是由于针对不同的平台,我们访问的方式不尽相同,thread_info封装了通用的逻辑来访问task_struct

上面说明了进程相关的结构体,值得注意的是在上图中其实还包含一个esp指针,该指针是cpu栈的指针,用来存放栈顶单元的地址。这样我们就 可以将进程在cpu上调度的整体流程给串起来了:esp指针 -> thread_info -> task_struct -> 进程运行时信息(包含调度等) ,至此就可以通过时钟中断定期去驱动对应的进程执行。

进程调度队列相关

进程调度还涉及的一个概念就是队列了,当下的计算机一般都是多cpu、多核,每个cpu都对应了运行队列、等待队列

  • 运行队列:当调度新的进程在cpu上运行的时候,内核只会从task_running队列上取,内核 中建立了多个可运行进程链表(也就是说一个cpu对应了多个链表),每种优先级的进程对应了一个链表,根据进程优先级 划分的话,大概可以分为以下三种:
    • 实时进程队列(0-99)
    • 交互进程
    • 批处理进程(后台进程)
  • 等待队列: 等待队列在内核中有很多的用处,如中断处理、进程同步、定时任务。

接下来描述以下进程创建的关系,进程创建子进程的时候会出现父子关系,当一个进程创建多个进程的时候,被创建的进程之间就会 产生兄弟关系。进程0(内核初始化的时候捏造出来的init_task,名字为swapper)和进程1(通过 kernel_thread创建 出,最终会调用exec(/sbin/init))是内核创建的,其中进程1是所有进程的祖先。

task_struct中包含的real_parent、parent、children、sibling就是用来描述进程中之间的这种关系,需要说明的是real_parent和 parent大部分情况下都是相同的,只有进程在调试的时候parent会指向调试进程的task_struct。

进程的创建

前面铺垫了这么多就是为了引出进程的创建,一般情况下子进程在创建的时候是会复制父进程的所有的资源的,但是拷贝父进程的整个地址空间 其实是一个非常缓慢的过程,因此在实际的情况中存在一些优化技巧:

  • 写时复制(copy on write):父子进程共享读的页面,两者中任一进程尝试写一个物理页面的时候,内核就把这个页的内容拷贝到一个新的物理页,并 把这个新的页面分配给这个写的进程
  • 轻量级进程:这种情况允许父子进程共享很多内核数据结构,如页表、打开的文件表等信息

上面是进程在创建完成之后的一些优化的技巧,不过真正的创建是需要一系列的系统调用完成的,具体过程如下:

  • fork: fork其实只是一个统称,作用就是复制父进程的结构,真实的情况包含以下几种:
    • clone:这种情况可以将资源选择性的复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,极端情况下会创建出一个线程(不复制任何资源 只通过指针进行共享)
    • fork: 父进程的所有资源都复制给子进程,区别上面的clone,fork是没有参数的,毕竟不给选择复制的机会
    • vfork: 与fork的作用相同,也是不带参数的。不过vfork不会拷贝父进程的页面表,出task_struct和堆栈之外,均通过指针进行遗传,一次vfork出来的是 线程而非进程,这是不传参数的另一种极端
  • exec: 这个时候子进程会加载真正的程序并运行

上面提到的fork的主体是copy_process,执行完copy_process之后,子进程开始运行,copy_process对应的功能如下:

  • 调用dup_task_struct,创建一个新的内核栈、新进程的thread_info、task_struct结构体,结构体中的成员变量 的值和当前进程的相同,此时父子进程的描述符是相同的
  • 检查子进程是否超过当前用户资源限制
  • 此时需要将子进程和父进程区别开来,进程描述符成员变量将会被清除,并设置初始值,此时子进程处于task_uninterruptible,确保子进程 不会被立即执行(毕竟还有初始化的工作没有完成)
  • 调用copy_files来更新task_struct中的flags:pf_forknoexec:表示进程还没有调用exec等
  • 调用get_pid分配一个pid给子进程
  • 根据clone的标志,copy_progress共享或者复制打开的文件、文件系统信息、信号处理方式、命名空间以及进程地址空间
  • 接下来将剩余的时间片在父子进程之间进行平分
  • 最后copy_progress执行完成之后就将子进程的指针返回给调用者,毕竟父子进程之间是存在一个挂载的关系:也即是父进程中存放了一个指向子进程的指针

上面就是进程创建的详细流程,在copy_progress成功返回之后,wake_up_new_task会将新创建的进程放到当前cpu的运行队列上面,这样子进程 就可以被调度执行了。在子进程创建完成后父进程可以选择退出、分开运行或者等待子进程运行结束在运行。

进程调度

调度时机

  • 进程状态转换的时候:进程进入sleep、exit等函数进行状态转换,这些函数会主动调用调度程序进行进程调度
  • 当前进程的时间片用完:进程的时间片是由时钟中断来更新的
  • 进程从中断、异常、系统调用返回到用户态的时候:进程在返回到用户态的时候都会调用ret_from_sys_call,这个函数会 主动调用调度程序进行进程的调度,原因是由于状态转换需要花一定的时间,因此,在返回到用户态前可以把内核态该处理的事情处理完

具体的调度过程就是采用一定的策略从cpu的runqueue中选择一个进程来运行,其中进程调度器会涉及的函数如下:

  • scheduler_tick:保持当前进程的time_slice为最新
  • try_to_wake_up:唤醒一个睡眠的进程
  • recalc_task_prio:更新进程的动态优先级
  • schedule:选择一个新的进程来运行
  • load_balance:使多处理器系统中的runqueue尽量平衡

小结

到此大概完成了进程的基本分析,参考文章: https://www.cnblogs.com/cuckoo-/p/10966158.html