Linux内核分析之进程

韩乔落

进程原理及系统调用

参考链接-Linux中国

进程四要素

  • 要有一段程序供该进程运行
  • 进程专用的系统堆栈空间
  • 进程控制块 (PCB),具体实现是task_struct结构
  • 有独立的存储空间

只具备前三点可称之为线程,完全没有用户地址空间的被称为内核线程,共享用户地址空间的被称为用户线程。

进程一般分为两大类:实时进程和普通进程。实时进程与普通进程的根本不同之处:如果系统中有一个实时进程且可运行,那么调度器总是会选择它,除非另有一个优先级更高的实时进程。

内核线程

内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行(实际上,也并行于内核自身的执行)。内核线程经常称之为(内核)守护进程(Daemon)。它们用于执行下列任务。

  • 周期性地将修改的内存页与页来源块设备同步(例如,使用mmap的文件映射)。
  • 如果内存页很少使用,则写入交换区。
  • 管理延时动作(deferred action)。
  • 实现文件系统的事务日志。

Linux内核提供的进程API

其定义在/include/linux/sched.h 中,下面介绍几个常用的:

1
2
3
4
5
#define TASK_RUNNING				0x0000 // 可运行或可就绪状态。
#define TASK_INTERRUPTIBLE 0x0001 // 浅睡眠,可中断睡眠状态。
#define TASK_UNINTERRUPTIBLE 0x0002 // 深睡眠,不可中断睡眠状态
#define __TASK_STOPPED 0x0004 // 终止状态
define EXIT_ZOMBIE 0x0020 // 僵尸状态

task_struct 结构体分析

task_struct 为进程描述符,Linux内核涉及进程和程序的所有算法都离不开这个结构体,源码在/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
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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
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, 运行态: 0, 终止态: >0 */
volatile long 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;
/* ptrace系统调用, 常被用于追踪调试 */
unsigned int ptrace;
// 条件编译,多处理器用到
#ifdef CONFIG_SMP
struct llist_node wake_entry;
int on_cpu;
#ifdef CONFIG_THREAD_INFO_IN_TASK
/* Current CPU: */
unsigned int cpu;
#endif
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; // 实时优先级

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_UCLAMP_TASK
/* Clamp values requested for a scheduling entity */
struct uclamp_se uclamp_req[UCLAMP_CNT];
/* Effective clamp values used for a scheduling entity */
struct uclamp_se uclamp[UCLAMP_CNT];
#endif

#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;
int nr_cpus_allowed;
const cpumask_t *cpus_ptr; // 允许进程在哪个cpu运行
cpumask_t cpus_mask;

#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;
#endif /* #ifdef CONFIG_TASKS_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
// 下面两个指向内存描述符,一般指向同一个内存描述符
// 内核线程 mm->null, active_mm 指向从进程借用的内存描述符(前一个进程的active_mm),
struct mm_struct *mm;
struct mm_struct *active_mm;

/* Per-thread vma caching: */
struct vmacache vmacache;

#ifdef SPLIT_RSS_COUNTING
struct task_rss_stat rss_stat;
#endif
// 进程状态参数
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_remote_wakeup:1;
#ifdef CONFIG_PSI
unsigned sched_psi_wake_requeue:1;
#endif

/* 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_SIGMASK
unsigned restore_sigmask:1;
#endif
#ifdef CONFIG_MEMCG
unsigned in_user_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
/* to be used once the psi infrastructure lands upstream. */
unsigned use_memdelay:1;
#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; // 指向父进程,ptrace会用到

/*
* 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]; /* pid_link指向了和该task_struct结构体相关的pid结构体。
* 进程号 进程组标识符,全局会话标识符
*/
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_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;

/* 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 setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN]; // 进程名称

struct nameidata *nameidata;

// 下面两个分别是 unix 信号量和共享内存
#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; // 打开文件表

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

/* 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_DEBUG_ATOMIC_SLEEP
int non_block_count;
#endif

#ifdef CONFIG_TRACE_IRQFLAGS
unsigned 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 48UL
u64 curr_chain_key;
int lockdep_depth;
unsigned int lockdep_recursion;
struct held_lock held_locks[MAX_LOCK_DEPTH];
#endif

#ifdef CONFIG_UBSAN
unsigned 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;

#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;
/* 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_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
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
#ifdef CONFIG_NUMA
/* Protected by alloc_lock: */
struct mempolicy *mempolicy;
short il_prev;
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_sig;
/*
* RmW on rseq_event_mask must be performed atomically
* with respect to preemption.
*/
unsigned long rseq_event_mask;
#endif

struct tlbflush_unmap_batch tlb_ubc;

union {
refcount_t rcu_users;
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_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;

#ifdef CONFIG_KASAN
unsigned 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
/* 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;
#endif

#ifdef CONFIG_MEMCG
struct 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_CGROUP
struct request_queue *throttle_queue;
#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
#ifdef CONFIG_DEBUG_ATOMIC_SLEEP
unsigned long task_state_change;
#endif
int pagefault_disabled;
#ifdef CONFIG_MMU
struct task_struct *oom_reaper_list;
#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_GCC_PLUGIN_STACKLEAK
unsigned long lowest_stack;
unsigned long prev_lowest_stack;
#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!
*/
};

进程优先级

Linux 进程 分为 3 种类型 , “ 限期进程 “ , “ 实时进程 “ , “ 普通进程 “ ; 从 “ 进程优先级 “ 角度对比 , 优先级从高到低分别是 : 限期进程 > 实时进程 > 普通进程 ;

  • 限期进程: 优先级为 -1
  • 实时进程:动态优先级为0-99的进程,采用实时调度算法调度。
  • 普通进程:动态优先级为100-139的进程,采用完全公平调度算法调度。
  • nice值:是用于调整普通进程优先级的参数。范围:-20-19。

一般来说优先级顺序,限期进程 > 实时进程 > 普通进程。

查看进程优先级

执行 ps -elf 命令查看进程优先级,PRI:进程优先级,数值越小,优先级越高。(并非动态优先级)NI:nice值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ ps -elf
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 6 80 0 - 25316 - 15:12 ? 00:00:01 /sbin/init auto nopro
1 S root 2 0 0 80 0 - 0 - 15:12 ? 00:00:00 [kthreadd]
1 I root 3 2 0 60 -20 - 0 - 15:12 ? 00:00:00 [rcu_gp]
1 I root 4 2 0 60 -20 - 0 - 15:12 ? 00:00:00 [rcu_par_gp]
1 I root 5 2 0 60 -20 - 0 - 15:12 ? 00:00:00 [slub_flushwq]
1 I root 6 2 0 60 -20 - 0 - 15:12 ? 00:00:00 [netns]
1 I root 7 2 0 80 0 - 0 - 15:12 ? 00:00:00 [kworker/0:0-events]
1 I root 8 2 0 60 -20 - 0 - 15:12 ? 00:00:00 [kworker/0:0H-events_
5 I root 9 2 0 80 0 - 0 - 15:12 ? 00:00:00 [kworker/0:1-events]
5 I root 10 2 4 80 0 - 0 - 15:12 ? 00:00:01 [kworker/u256:0-event
1 I root 11 2 0 60 -20 - 0 - 15:12 ? 00:00:00 [mm_percpu_wq]
......

进程优先级策略

  • prio(动态优先级)

动态优先级,有效优先级,调度器最终使用的优先级数值,范围0-139,值越小,优先级越高。用于保存静态优先级, 是进程启动时分配的优先级, 可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定

  • static_prio(静态优先级)

静态优先级,采用SCHED_NORMALSCHED_BATCH调度策略的进程(即普通进程)用于计算动态优先级的,范围100-139。进程的动态优先级, 这个有显示才是调度器重点考虑的进程优先级

prio = static_prio = nice + DEFAULT_PRIO = nice + 120

  • normal_prio(归一化优先级)

用于计算prio的中间变量,不需要太关心。普通进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级, 可以通过normal_prio来计算(非实时进程用static_prIo计算, 实时进程用rt_priority计算)

  • rt_priority(实时优先级)

实时优先级,采用SCHED_FIFOSCHED_RR调度策略进程(即实时进程)用于计算动态优先级,范围0-99。实时优先级数值越大,得到的动态优先级数值越小,优先级越高。

prio = MAX_RT_PRIO - 1 - rt_prio = 100 - 1 - rt_priority

prio

一些进程相关系统调用

fork(), vfork(), clone() 函数实际上就是系统调用。

调用 描述
clone 创建轻量级进程(也就是线程),pthread库基于此实现
vfork 父子进程共享资源,子进程先于父进程执行
fork 创建父进程的完整副本

kernel_fork

退出进程

正常退出

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

异常退出

  • 调用abort
  • 由信号终止

调度器简述

调度器

  • 调度:就是按照某种调度的算法设计,从进程的就绪队列中选择进程分配CPU,主要是协调进程对CPU等相关资源的使用。
  • 调度的目的:最大限度的使用CPU时间。

​ Linux内核中用来安排调度进程执行的模块称为调度器(Scheduler),它可以切换进程状态(执行、睡眠、退出等)。调度器相当于CPU的管理员,主要完成两件事:

  1. 选择某些就绪进程来执行
  2. 打断某些执行的进程让他们变为就绪态

​ 操作系统还负责“上下文切换”,即保存切换前的寄存器内容等进程的状态,以便稍后恢复。如果调度器支持就绪状态切换到执行状态,同时支持执行状态切换为就绪状态,就称该调度器为抢占式调度器。

调度类结构体

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
struct sched_class {
/* OS中有多个调度类,按照调度优先级排成一个链表 */
const struct sched_class *next;

#ifdef CONFIG_UCLAMP_TASK
int uclamp_enabled;
#endif
/* 将进程加入到执行队列中,即将调度实体存放到红黑树中,并将nr_running变量+1 */
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
/* 从执行队列删除进程,并将nr_running变量-1*/
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
/* 放弃CPU的执行权限,实际上此函数执行先出队后入队,这种情况下它直接将调度实体存放在红黑树的最右端 */
void (*yield_task) (struct rq *rq);
bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);
/* 专门用于检查当前进程是否可被新进程抢占 */
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
/* 选择下一个要运行的进程 */
struct task_struct *(*pick_next_task)(struct rq *rq);
/* 将进程加到运行队列中 */
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);

#ifdef CONFIG_SMP
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);

void (*task_woken)(struct rq *this_rq, struct task_struct *task);

void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);

void (*rq_online)(struct rq *rq);
void (*rq_offline)(struct rq *rq);
#endif

void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
void (*task_fork)(struct task_struct *p);
void (*task_dead)(struct task_struct *p);

/*
* The switched_from() call is allowed to drop rq->lock, therefore we
* cannot assume the switched_from/switched_to pair is serliazed by
* rq->lock. They are however serialized by p->pi_lock.
*/
void (*switched_from)(struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
int oldprio);

unsigned int (*get_rr_interval)(struct rq *rq,
struct task_struct *task);

void (*update_curr)(struct rq *rq);

#define TASK_SET_GROUP 0
#define TASK_MOVE_GROUP 1

#ifdef CONFIG_FAIR_GROUP_SCHED
void (*task_change_group)(struct task_struct *p, int type);
#endif
};

调度器分类

调度器可分为五种,对应不同的调度策略:

1
2
3
4
5
extern const struct sched_class stop_sched_class; // 停机调度类
extern const struct sched_class dl_sched_class; // 期限调度类
extern const struct sched_class rt_sched_class; // 实时调度类
extern const struct sched_class fair_sched_class; // 公平调度类/cfs
extern const struct sched_class idle_sched_class; // 空闲调度类

这五种调度类优先级从高到低依次为:停机调度类>限期调度类>实时调度类>公平调度类>空闲调度类

  • 停机调度类

​ 优先级最高的调度类,停机进程是优先级最高的进程,可以抢占所有其他进程,其他进程不可能抢占停机进程,停机的意思是使处理器停下来,做更紧急的事情。

  • 限期调度类

​ 最早使用优先算法。使用红黑树把进程按照绝对截止限期从小到达排序,每次调度时选择绝对截止期限最小的进程。如果限期进程用完了它的运行时间,它将让出处理器,并且把它从运行队列中删除。在下一个周期开始,重新把它添加到运行队列中。

  • 实时调度类

​ 为每个调度优先级维护一个队列。SCHED_FIFO实现了一种简单的、先进先出的调度算法。它不使用时间片。处于可运行状态的SCHED_FIFO级的进程会比任何SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO级进程处于可运行状态,就会一直执行,直到它自己受阻塞或显式地释放处理器为止。 它不基于时间片,可以一直执行下去。只有更高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。如果有两个或者更多的同优先级的SCHED_FIFO级进程,它们会轮流执行,但是依然只有在它们愿意让出处理器时才会退出。只有有SCHED_FIFO级进程在执行,其他级别较低的进程就只能等待它变为不可运行后才有机会执行。

  • 实时调度类

​ SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了。也就是说,SCHED_RR是带有时间片的SCHED_FIFO——这是一种实时轮流调度算法。

  • 公平调度类

​ 使用完全公平调度算法。完全公平调度算法引入虚拟运行时间的相关概念:虚拟运行时间 = 实际运行时间 * nice0对应的权重 / 进程的权重

  • 空闲调度类

​ 每个CPU上都有一个空闲线程,即0号线程,空闲调度类优先级别最低,当没有其他进程可以调度的时候,才会调度空闲线程。

就绪队列结构体

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
struct rq {
/* runqueue lock: */
raw_spinlock_t lock;

/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned int nr_running;
#ifdef CONFIG_NUMA_BALANCING
unsigned int nr_numa_running;
unsigned int nr_preferred_running;
unsigned int numa_migrate_on;
#endif
#ifdef CONFIG_NO_HZ_COMMON
#ifdef CONFIG_SMP
unsigned long last_load_update_tick;
unsigned long last_blocked_load_update_tick;
unsigned int has_blocked_load;
#endif /* CONFIG_SMP */
unsigned int nohz_tick_stopped;
atomic_t nohz_flags;
#endif /* CONFIG_NO_HZ_COMMON */

unsigned long nr_load_updates;
u64 nr_switches;

#ifdef CONFIG_UCLAMP_TASK
/* Utilization clamp values based on CPU's RUNNABLE tasks */
struct uclamp_rq uclamp[UCLAMP_CNT] ____cacheline_aligned;
unsigned int uclamp_flags;
#define UCLAMP_FLAG_IDLE 0x01
#endif

struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;

#ifdef CONFIG_FAIR_GROUP_SCHED
/* list of leaf cfs_rq on this CPU: */
struct list_head leaf_cfs_rq_list;
struct list_head *tmp_alone_branch;
#endif /* CONFIG_FAIR_GROUP_SCHED */

/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;

struct task_struct __rcu *curr;
struct task_struct *idle;
struct task_struct *stop;
unsigned long next_balance;
struct mm_struct *prev_mm;

unsigned int clock_update_flags;
u64 clock;
/* Ensure that all clocks are in the same cache line */
u64 clock_task ____cacheline_aligned;
u64 clock_pelt;
unsigned long lost_idle_time;

atomic_t nr_iowait;

#ifdef CONFIG_MEMBARRIER
int membarrier_state;
#endif

#ifdef CONFIG_SMP
struct root_domain *rd;
struct sched_domain __rcu *sd;

unsigned long cpu_capacity;
unsigned long cpu_capacity_orig;

struct callback_head *balance_callback;

unsigned char idle_balance;

unsigned long misfit_task_load;

/* For active balancing */
int active_balance;
int push_cpu;
struct cpu_stop_work active_balance_work;

/* CPU of this runqueue: */
int cpu;
int online;

struct list_head cfs_tasks;

struct sched_avg avg_rt;
struct sched_avg avg_dl;
#ifdef CONFIG_HAVE_SCHED_AVG_IRQ
struct sched_avg avg_irq;
#endif
u64 idle_stamp;
u64 avg_idle;

/* This is used to determine avg_idle's max value */
u64 max_idle_balance_cost;
#endif

#ifdef CONFIG_IRQ_TIME_ACCOUNTING
u64 prev_irq_time;
#endif
#ifdef CONFIG_PARAVIRT
u64 prev_steal_time;
#endif
#ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING
u64 prev_steal_time_rq;
#endif

/* calc_load related fields */
unsigned long calc_load_update;
long calc_load_active;

#ifdef CONFIG_SCHED_HRTICK
#ifdef CONFIG_SMP
int hrtick_csd_pending;
call_single_data_t hrtick_csd;
#endif
struct hrtimer hrtick_timer;
#endif

#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
unsigned long long rq_cpu_time;
/* could above be rq->cfs_rq.exec_clock + rq->rt_rq.rt_runtime ? */

/* sys_sched_yield() stats */
unsigned int yld_count;

/* schedule() stats */
unsigned int sched_count;
unsigned int sched_goidle;

/* try_to_wake_up() stats */
unsigned int ttwu_count;
unsigned int ttwu_local;
#endif

#ifdef CONFIG_SMP
struct llist_head wake_list;
#endif

#ifdef CONFIG_CPU_IDLE
/* Must be inspected within a rcu lock section */
struct cpuidle_state *idle_state;
#endif
};

优先级

task_struct结构体中采用三个成员表示进程的优先级:prio 和 normal_prio(动态优先级)、static_prio(静态优先级)。

1
2
3
4
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)
#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)

实时进程优先级从 0 ~ MAX_USER_RT_PRIO-1(99),普通进程优先级从 100 ~ MAX_RT_PRIO-1(139)

内核调度策略

Linux内核提供一些调度策略供用户应用程序来选择调度器。

1
2
3
4
5
6
7
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
  • SCHED_NORMAL:普通进程的调度策略,使task选择CFS调度器来调度运行
  • SCHED_FIFO:实时进程的调度策略,先进先出调度,没有时间片,没有更高优先级的状态下,只有等待主动让出CPU(非抢占)
  • SCHED_RR:实时进程的调度策略,采用时间片轮转,进程使用完时间片之后会进入优先级对应运行队列的尾部,把CPU让给同等优先级的其他进程
  • SCHED_BATCH:普通进程的调度策略,批量处理,使task选择CFS调度器来调度运行
  • SCHED_IDLE:普通进程的调度策略,使我们task以最低优先级选择CFS调度器来调度运行
  • SCHED_DEADLINE:限期进程调度策略,使我们task选择Deadline调度器来调度运行

stop调度器和Deadline调度器,仅使用于内核,用户没有办法进行选择

ddqandcl

调度实体

调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPUI时间首先在一半的进程组(比如, 所有进程按照所有者分组)之间分配, 接下来分配的时间再在组内进行二次分配。这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组。

linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体

ddst

CFS调度实体 sched_entity

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
// 首先需要注意的是,调度实体可能是进程和调度组两种,因此结构体中会同时包含这两类实体相关的数据。 

struct sched_entity {
/*
* load 表示当前调度实体的权重,这个权重决定了一个调度实体的运行优先级,对进程实体而言,
* 它是由静态优先级计算得到,对应调度组而言,是组内各实体的 load 之和。
* load 和 cpu_load 两个名字取得是有歧义的,虽然都是 load,但是 cpu_load 却是表示负载
*/
struct load_weight load;
/*
* 红黑树的数据节点,使用该 rb_node 将当前节点挂到红黑树上面,还是内核中的老套路,
* 将 rb_node 嵌入 sched_entity 结构,在操作节点时,可以通过 rb_node 反向获取到其父结构。
*/
struct rb_node run_node;
/*
* 链表节点,被链接到 percpu 的 rq->cfs_tasks 上,
* 在做 CPU 之间的负载均衡时,
* 就会从该链表上选出 group_node 节点作为迁移进程。
*/
struct list_head group_node;
// 标志位,代表当前调度实体是否在就绪队列上
unsigned int on_rq;
// 当前实体上次被调度执行的时间
u64 exec_start;
// 当前实体总执行时间
u64 sum_exec_runtime;
/*
* 截止到上次统计,进程执行的时间,通常,
* 通过 sum_exec_runtime - prev_sum_exec_runtime 来统计进程本次在 CPU 上执行了多长时间,
* 以执行某些时间相关的操作
*/
u64 prev_sum_exec_runtime;
// 当前实体的虚拟时间,调度器就是通过调度实体的虚拟时间进行调度,在选择下一个待执行实体时总是选择虚拟时间最小的。
u64 vruntime;
/*
* 实体执行迁移的次数,在多核系统中,CPU 之间会经常性地执行负载均衡操作,
* 因此调度实体很可能因为负载均衡而迁移到其它 CPU 的就绪队列上。
*/
u64 nr_migrations;
// 进程的属性统计,需要内核配置 CONFIG_SCHEDSTATS,其统计信息包含睡眠统计、等待延迟统计、CPU迁移统计、唤醒统计等。
struct sched_statistics statistics;

#ifdef CONFIG_FAIR_GROUP_SCHED
/*
* 由于调度实体可能是调度组,调度组中存在嵌套的调度实体,
* 这个标志表示当前实体处于调度组中的深度,当不属于调度组时, depth 为 0.
*/
int depth;
// 指向父级调度实体
struct sched_entity *parent;
// 当前调度实体属于的 cfs_rq.
struct cfs_rq *cfs_rq;
/*
* 如果当前调度实体是一个调度组,那么它将拥有自己的 cfs_rq,属于该组内的所有调度实体在该 cfs_rq 上排列,
* 而且当前 se 也是组内所有调度实体的 parent,子 se 存在一个指针指向 parent,
* 而父级与子 se 的联系并不直接,而是通过访问 cfs_rq 来找到对应的子 se。
*/
struct cfs_rq *my_q;
#endif

#ifdef CONFIG_SMP
/*
* 在多核系统中,需要记录 CPU 的负载,其统计方式精确到每一个调度实体,
* 而这里的 avg 成员就是用来记录当前实体对于 CPU 的负载贡献。
*/
struct sched_avg avg ____cacheline_aligned_in_smp;
#endif
};

RT调度实体 sched_rt_entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct sched_rt_entity {
struct list_head run_list; // 用于加入到优先级队列中
unsigned long timeout; // 设置时间超时
unsigned long watchdog_stamp; // 用于 记录 jiffies 的值
unsigned int time_slice; // 表示 时间片
unsigned short on_rq; //
unsigned short on_list;

struct sched_rt_entity *back; // 用于由上到下连接 " 实时调度实体 "
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity *parent; // 指向父类 " 实时调度实体 "
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq; // 表示 "实时调度实体" 所属的 "实时运行队列 "
/* rq "owned" by this entity/group: */
struct rt_rq *my_q; // 表示 "实时调度实体" 所拥有的 "实时运行队列" , 用于管理 " 子任务 "
#endif
} __randomize_layout;

DL调度实体 sched_dl_entity

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
struct sched_dl_entity {
struct rb_node rb_node;

/*
* Original scheduling parameters. Copied here from sched_attr
* during sched_setattr(), they will remain the same until
* the next sched_setattr().
*/
u64 dl_runtime; /* Maximum runtime for each instance */
u64 dl_deadline; /* Relative deadline of each instance */
u64 dl_period; /* Separation of two instances (period) */
u64 dl_bw; /* dl_runtime / dl_period */
u64 dl_density; /* dl_runtime / dl_deadline */

/*
* Actual scheduling parameters. Initialized with the values above,
* they are continuously updated during task execution. Note that
* the remaining runtime could be < 0 in case we are in overrun.
*/
s64 runtime; /* Remaining runtime for this instance */
u64 deadline; /* Absolute deadline for this instance */
unsigned int flags; /* Specifying the scheduler behaviour */

/*
* Some bool flags:
*
* @dl_throttled tells if we exhausted the runtime. If so, the
* task has to wait for a replenishment to be performed at the
* next firing of dl_timer.
*
* @dl_boosted tells if we are boosted due to DI. If so we are
* outside bandwidth enforcement mechanism (but only until we
* exit the critical section);
*
* @dl_yielded tells if task gave up the CPU before consuming
* all its available runtime during the last job.
*
* @dl_non_contending tells if the task is inactive while still
* contributing to the active utilization. In other words, it
* indicates if the inactive timer has been armed and its handler
* has not been executed yet. This flag is useful to avoid race
* conditions between the inactive timer handler and the wakeup
* code.
*
* @dl_overrun tells if the task asked to be informed about runtime
* overruns.
*/
unsigned int dl_throttled : 1;
unsigned int dl_boosted : 1;
unsigned int dl_yielded : 1;
unsigned int dl_non_contending : 1;
unsigned int dl_overrun : 1;

/*
* Bandwidth enforcement timer. Each -deadline task has its
* own bandwidth to be enforced, thus we need one timer per task.
*/
struct hrtimer dl_timer;

/*
* Inactive timer, responsible for decreasing the active utilization
* at the "0-lag time". When a -deadline task blocks, it contributes
* to GRUB's active utilization until the "0-lag time", hence a
* timer is needed to decrease the active utilization at the correct
* time.
*/
struct hrtimer inactive_timer;
};

CFS调度类fair_sched_class

完全公平调度算法体现在对待每个进程都是公平的,让每个进程都运行一段相同的时间片,这就是基于时间片轮询调度算法。CFS定义一种新调度模型,它给cfs_rq(cfs的run queue)中的每一个进程都设置一个虚拟时钟 - virtual runtime(vruntime),如果一个进程得以执行,随着执行时间的不断增长,其vruntime也不断增大,没有得到执行的进程vruntime将保持不变。

1
const struct sched_class *sched_class; // 表示该进程所属的调度器类

CFS:完全公平调度器。实际当中,必然会有进程优先级高或进程优先级低,此时CFS调度器会引入权重,使用该权重代表进程的优先级。各个进程会按照权重的比例来分配CPU时间。

实际运行时间 = 调度周期 * 进程权重 / 所有进程权重之和

虚拟运行时间 = 实际运行时间 * NICE_0_LOAD / 进程权重

假设有X和Y两个进程,X权重为1024,Y权重为2048,那么X所获得CPU时间的比例为(1024 / (1024 + 2048))* 100%

​ 在一个调度周期里面,所有进程的虚拟运行时间都是不变的,所以在进程调度时,只需要找到虚拟运行时间最小的进程调度运行即可。Linux采用红黑树保存调度实体,按照虚拟时间从小到大存储在红黑树中。

diaoducelv

主调度器:通过调用schedule()函数来完成进程的选择和切换。

周期性调度器:根据频率自动调用scheduler_tick函数,根据进程运行时间触发调度

上下文切换:主要做两个事情(切换地址空间、切换寄存器和栈空间)


CFS调度器的Linux部分内核源码:

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
const struct sched_class fair_sched_class = {
.next = &idle_sched_class, // CPU运行队列中的下一个调度类,优先级更低,为空闲调度类
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,
.yield_to_task = yield_to_task_fair,

.check_preempt_curr = check_preempt_wakeup,

.pick_next_task = __pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
.set_next_task = set_next_task_fair,

#ifdef CONFIG_SMP
.balance = balance_fair,
.select_task_rq = select_task_rq_fair,
.migrate_task_rq = migrate_task_rq_fair,

.rq_online = rq_online_fair,
.rq_offline = rq_offline_fair,

.task_dead = task_dead_fair,
.set_cpus_allowed = set_cpus_allowed_common,
#endif

.task_tick = task_tick_fair,
.task_fork = task_fork_fair,

.prio_changed = prio_changed_fair,
.switched_from = switched_from_fair,
.switched_to = switched_to_fair,

.get_rr_interval = get_rr_interval_fair,

.update_curr = update_curr_fair,

#ifdef CONFIG_FAIR_GROUP_SCHED
.task_change_group = task_change_group_fair,
#endif

#ifdef CONFIG_UCLAMP_TASK
.uclamp_enabled = 1,
#endif
};

CFS就绪队列cfs_rq

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
struct cfs_rq {
// 在上面的 sched_entity 结构中也存在同样的 load 成员,一个 sched_entity(se) 的 load 成员表示单个 se 的 load,
// 而 cfs_rq 上的 load 表示当前 cfs_rq 上所有实体的 load 总和。
struct load_weight load;

// 这两个都是对当前 cfs_rq 上实体的统计,区别在于:nr_running 只表示当前 cfs_rq 上存在的子实体,
// 如果子实体是调度组,也只算一个。而 h_nr_running 的统计会递归地包含子调度组中的所有实体。
// 因此可以通过比较这两者是否相等来判断当前 cfs_rq 上是否存在调度组。
unsigned int nr_running, h_nr_running;

// 当前 cfs_rq 上执行的时间
u64 exec_clock;

// 这是一个非常重要的成员,每个 cfs_rq 都会维护一个最小虚拟时间 min_vruntime,这个虚拟时间是一个基准值,
// 每个新添加到当前队列的 se 都会被初始化为当前的 min_vruntime 附近的值,
// 以保证新添加的执行实体和当前队列上已存在的实体拥有差不多的执行机会,至于执行多长时间,
// 则是由对应实体的 load 决定,该 load 会决定 se->vruntime 的增长速度。
u64 min_vruntime;

// cfs_rq 维护的红黑树结构,其中包含一个根节点以及最左边实体(vruntime最小的实体,对应一个进程)的指针。
struct rb_root_cached tasks_timeline;

/*
* 'curr' points to currently running entity on this cfs_rq.
* It is set to NULL otherwise (i.e when none are currently running).
*/
// 记录当前 cfs_rq 上特殊的几个实体指针:
// curr:cfs_rq 上当前正在运行的实体,如果运行的进程实体不在当前 cfs_rq 上,设置为 NULL。
// 需要注意的是,在支持组调度的情况下,一个进程 se 运行,被设置为当前 cfs_rq 的 curr,
// 同时其 parent 也会被设置为同级 cfs_rq 的 curr.
// next:用户特别指定的需要在下一次调度中执行的进程实体,但是这并不是绝对的,
// 只有在 next 指定的进程实体快要运行(但可能不是下次)的时候,因为这时候不会造成太大的不公平,
// 就会运行指定的 next,也是一种临时提高优先级的做法。
// last:上次执行过的实体不应该跨越公平性原则执行,比如将 next 设置为 last,这时候就需要仔细斟酌一下了,
// 也是保证公平性的一种方法。
struct sched_entity *curr, *next, *last, *skip;


#ifdef CONFIG_SMP

// 在多核 CPU 中,对当前 cfs_rq 的负载统计,该统计会精确到每个 se,自然也就会传递到 cfs_rq,
// 下面的几个成员用于负载统计,目前专注于 cfs 调度的主要实现,负载均衡部分后续再进行分析。
struct sched_avg avg;
u64 runnable_load_sum;
unsigned long runnable_load_avg;
atomic_long_t removed_load_avg, removed_util_avg;


#ifdef CONFIG_FAIR_GROUP_SCHED
// 指向 percpu rq 的指针,在不支持组调度的系统中,runqueue 上只存在一个 cfs_rq,
// 可以直接结构体的地址偏移反向获取到 rq 的指针,而支持组调度的 cfs_rq 可能是 root cfs_rq 的子级 cfs_rq,
// 因此需要通过一个指针获取当前 cfs_rq 所在的 rq。
struct rq *rq;

// 一部分是组调度中的带宽控制,在某些应用场景,比如虚拟化或者用户付费使用服务器中,需要对用户的使用带宽或者时长进行限制,
// 就需要用到 cfs 调度的贷款控制,其实现原理就是在一个周期内,通过某些算法设置用户组应该运行多长时间、
// 同时计算已经运行了多长时间,如果超时,就将该用户组 throttle,夺取其执行权直到下一个周期。
// 同样的,cfs 的带宽控制部分我们暂时不深入讨论。
#ifdef CONFIG_CFS_BANDWIDTH
int runtime_enabled;
int expires_seq;
u64 runtime_expires;
s64 runtime_remaining;

u64 throttled_clock, throttled_clock_task;
u64 throttled_clock_task_time;
int throttled, throttle_count;
struct list_head throttled_list;
#endif /* CONFIG_CFS_BANDWIDTH */
#endif /* CONFIG_FAIR_GROUP_SCHED */
};

cfs_rq : 跟踪就绪队列信息以及管理就绪态调度实体,并且维护一棵按照虚拟时间排序的红黑树

tasks_timeline->rb_root(红黑树的根)

tasks_timeline->rb_leftmost(指向虚拟时间最小的结点)

RT调度类rt_sched_class

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
const struct sched_class rt_sched_class = {
.next = &fair_sched_class,
.enqueue_task = enqueue_task_rt, // 将一个task放入到就绪队列头或者尾部
.dequeue_task = dequeue_task_rt, // 将一个task从就绪队列末尾
.yield_task = yield_task_rt, // 主动放弃执行

.check_preempt_curr = check_preempt_curr_rt,
// 核心调度器选择就绪队列的哪个任务将要被调度,prev是将要被调度出的任务,返回值是将要被调度的任务。
.pick_next_task = pick_next_task_rt,
.put_prev_task = put_prev_task_rt, // 当一个任务将要被调度出时执行

#ifdef CONFIG_SMP
.select_task_rq = select_task_rq_rt, // 核心调度器给任务选定CPU,用于将任务分发到不同CPU上执行。

.set_cpus_allowed = set_cpus_allowed_rt,
.rq_online = rq_online_rt,
.rq_offline = rq_offline_rt,
.post_schedule = post_schedule_rt,
.task_woken = task_woken_rt,
.switched_from = switched_from_rt,
#endif

.set_curr_task = set_curr_task_rt, // 当任务修改其调度类或修改其它任务组时,将调用这个函数。
.task_tick = task_tick_rt, // 当时钟中断出发时被调用,主要更新进程运行统计信息以及是否需要调度。

.get_rr_interval = get_rr_interval_rt,

.prio_changed = prio_changed_rt,
.switched_to = switched_to_rt,

.update_curr = update_curr_rt,
}

RT就绪队列struct rt_rq

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
struct rt_rq {
struct rt_prio_array active; // 优先级队列
unsigned int rt_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
struct {
int curr; /* highest queued rt task prio 最高实时任务的优先级 */
#ifdef CONFIG_SMP
int next; /* next highest 下一个实时任务最高优先级 */
#endif
} highest_prio;
#endif
#ifdef CONFIG_SMP
unsigned long rt_nr_migratory;
unsigned long rt_nr_total;
int overloaded;
struct plist_head pushable_tasks;
#endif
int rt_queued;

int rt_throttled // 当前队列的实时调度是否受限
u64 rt_time; // 当前队列的累积运行时间
u64 rt_runtime; // 当前队列的单个period周期内最大运行时间
/* Nests inside the rq lock: */
raw_spinlock_t rt_runtime_lock;

#ifdef CONFIG_RT_GROUP_SCHED
unsigned long rt_nr_boosted;

struct rq *rq;
struct task_group *tg;
#endif
}

DL调度类dl_sched_class

dl_sched_class

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
const struct sched_class dl_sched_class = {
.next = &rt_sched_class,
.enqueue_task = enqueue_task_dl,
.dequeue_task = dequeue_task_dl,
.yield_task = yield_task_dl,

.check_preempt_curr = check_preempt_curr_dl,

.pick_next_task = pick_next_task_dl,
.put_prev_task = put_prev_task_dl,
.set_next_task = set_next_task_dl,

#ifdef CONFIG_SMP
.balance = balance_dl,
.select_task_rq = select_task_rq_dl,
.migrate_task_rq = migrate_task_rq_dl,
.set_cpus_allowed = set_cpus_allowed_dl,
.rq_online = rq_online_dl,
.rq_offline = rq_offline_dl,
.task_woken = task_woken_dl,
#endif

.task_tick = task_tick_dl,
.task_fork = task_fork_dl,

.prio_changed = prio_changed_dl,
.switched_from = switched_from_dl,
.switched_to = switched_to_dl,

.update_curr = update_curr_dl,
};

DL就绪队列dl_rq

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
struct dl_rq {
/* runqueue is an rbtree, ordered by deadline */
struct rb_root_cached root;

unsigned long dl_nr_running;

#ifdef CONFIG_SMP
/*
* Deadline values of the currently executing and the
* earliest ready task on this rq. Caching these facilitates
* the decision whether or not a ready but not running task
* should migrate somewhere else.
*/
struct {
u64 curr;
u64 next;
} earliest_dl;

unsigned long dl_nr_migratory;
int overloaded;

/*
* Tasks on this rq that can be pushed away. They are kept in
* an rb-tree, ordered by tasks' deadlines, with caching
* of the leftmost (earliest deadline) element.
*/
struct rb_root_cached pushable_dl_tasks_root;
#else
struct dl_bw dl_bw;
#endif
/*
* "Active utilization" for this runqueue: increased when a
* task wakes up (becomes TASK_RUNNING) and decreased when a
* task blocks
*/
u64 running_bw;

/*
* Utilization of the tasks "assigned" to this runqueue (including
* the tasks that are in runqueue and the tasks that executed on this
* CPU and blocked). Increased when a task moves to this runqueue, and
* decreased when the task moves away (migrates, changes scheduling
* policy, or terminates).
* This is needed to compute the "inactive utilization" for the
* runqueue (inactive utilization = this_bw - running_bw).
*/
u64 this_bw;
u64 extra_bw;

/*
* Inverse of the fraction of CPU utilization that can be reclaimed
* by the GRUB algorithm.
*/
u64 bw_ratio;
};

体系结构

Symmetrical Multi-Processing,简称SMP,即对称多处理技术,是指将多CPU汇集在同一总线上,各CPU间进行内存和总线共享的技术。将同一个工作平衡地(run in parallel)分布到多个CPU上运行,该相同任务在不同CPU上共享着相同的物理内存。在现行的SMP架构中,发展出三种模型:UMA、NUMA和MPP。下面将展开重点讲述前两种模型。

UMA系统架构

SMP (Symmetric Multi Processing),对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。Access to RAM is serialized; this and cache coherency issues causes performance to lag slightly behind the number of additional processors in the system.

uma

所谓对称多处理器结构,是指服务器中多个 CPU 对称工作,无主次或从属关系。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。

SMP 服务器的主要特征是共享,系统中所有资源 (CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成 SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着 CPU 数量的增加,内存访问冲突将迅速增加,最终会造成 CPU 资源的浪费,使 CPU 性能的有效性大大降低。实验证明, SMP 服务器 CPU 利用率最好的情况是 2 至 4 个 CPU 。

NUMA系统架构

由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA(Non-Uniform Memory Access) 就是这种努力下的结果之一。利用 NUMA 技术,可以把几十个 CPU( 甚至上百个 CPU) 组合在一个服务器内。

numa

NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。

利用 NUMA 技术,可以较好地解决原来 SMP 系统的扩展问题,在一个物理服务器内可以支持上百个 CPU 。比较典型的 NUMA 服务器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。

但 NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。如 HP 公司发布 Superdome 服务器时,曾公布了它与 HP 其它 UNIX 服务器的相对性能值,结果发现, 64 路 CPU 的 Superdome (NUMA 结构 ) 的相对性能值是 20 ,而 8 路 N4000( 共享的 SMP 结构 ) 的相对性能值是 6.3 。从这个结果可以看到, 8 倍数量的 CPU 换来的只是 3 倍性能的提升。

MPP系统架构

和 NUMA 不同, MPP 提供了另外一种进行系统扩展的方式,它由多个 SMP 服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务,从用户的角度来看是一个服务器系统。其基本特征是由多个 SMP 服务器 ( 每个 SMP 服务器称节点 ) 通过节点互联网络连接而成,每个节点只访问自己的本地资源 ( 内存、存储等 ) ,是一种完全无共享 (Share Nothing) 结构,因而扩展能力最好,理论上其扩展无限制,目前的技术可实现 512 个节点互联,数千个 CPU 。目前业界对节点互联网络暂无标准,如 NCR 的 Bynet , IBM 的 SPSwitch ,它们都采用了不同的内部实现机制。但节点互联网仅供 MPP 服务器内部使用,对用户而言是透明的。

在 MPP 系统中,每个 SMP 节点也可以运行自己的操作系统、数据库等。但和 NUMA 不同的是,它不存在异地内存访问的问题。换言之,每个节点内的 CPU 不能访问另一个节点的内存。节点之间的信息交互是通过节点互联网络实现的,这个过程一般称为数据重分配 (Data Redistribution) 。

但是 MPP 服务器需要一种复杂的机制来调度和平衡各个节点的负载和并行处理过程。目前一些基于 MPP 技术的服务器往往通过系统级软件 ( 如数据库 ) 来屏蔽这种复杂性。举例来说, NCR 的 Teradata 就是基于 MPP 技术的一个关系数据库软件,基于此数据库来开发应用时,不管后台服务器由多少个节点组成,开发人员所面对的都是同一个数据库系统,而不需要考虑如何调度其中某几个节点的负载。

MPP (Massively Parallel Processing),大规模并行处理系统,这样的系统是由许多松耦合的处理单元组成的,要注意的是这里指的是处理单元而不是处理器。每个单元内的CPU都有自己私有的资源,如总线,内存,硬盘等。在每个单元内都有操作系统和管理数据库的实例复本。这种结构最大的特点在于不共享资源。

mpp

RCU 机制

Read-copy update (RCU) 是一种 2002 年 10 月被引入到内核当中的同步机制。通过允许在更新的同时读数据,RCU 提高了同步机制的可伸缩性(scalability)。相对于传统的在并发线程间不区分是读者还是写者的简单互斥性锁机制,或者是哪些允许并发读但同时不 允许写的读写锁,RCU 支持同时一个更新线程和多个读线程的并发。RCU 通过保存对象的多个副本来保障读操作的连续性,并保证在预定的读方临界区没有完成之前不会释放这个对象。RCU定义并使用高效、可伸缩的机制来发布并读取 对象的新版本,并延长旧版本们的寿命。这些机制将工作分发到了读和更新路径上,以保证读路径可以极快地运行。在某些场合(非抢占内核),RCU 的读方没有任何性能负担。

Linux内核源码当中,关于RCU的文档比较齐全,你可以在 Documentation/RCU/ 目录下找到这些文件。Paul E. McKenney 是内核中RCU源码的主要实现者,他也写了很多RCU方面的文章。他把这些文章和一些关于RCU的论文的链接整理到了一起:http://www2.rdrop.com/users/paulmck/RCU/

RCU机制解决了什么

在RCU的实现过程中,我们主要解决以下问题:

1、在读取过程中,另外一个线程删除了一个节点。删除线程可以把这个节点从链表中移除,但它不能直接销毁这个节点,必须等到所有的读取线程读取完成以后,才进行销毁操作。RCU中把这个过程称为宽限期(Grace period)。

2、在读取过程中,另外一个线程插入了一个新节点,而读线程读到了这个节点,那么需要保证读到的这个节点是完整的。这里涉及到了发布-订阅机制(Publish-Subscribe Mechanism)。

3、保证读取链表的完整性。新增或者删除一个节点,不至于导致遍历一个链表从中间断开。但是RCU并不保证一定能读到新增的节点或者不读到要被删除的节点。

RCU(Read-Copy Update),顾名思义就是读-拷贝更新,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。那么这个“适当的时机”是怎么确定的呢?这是由内核确定的,也是我们后面讨论的重点。

RCU 原理

RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。

读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。

RCU 既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。

读者在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。

RCU 核心 API

1
2
3
4
5
6
7
rcu_read_lock()
rcu_read_unlock()

synchronize_rcu()

rcu_assign_pointer()
rcu_dereference()

其中,rcu_read_lock()rcu_read_unlock()用来保持一个读者的RCU临界区.在该临界区内不允许发生上下文切换。

为什么不能发生切换呢?因为内核要根据“是否发生过切换”来判断读者是否已结束读操作,我们后面再分析。

而下列的函数用于实现内存屏障的作用。

  • rcu_dereference():读者调用它来获得一个被RCU保护的指针。
  • rcu_assign_pointer():写者使用该函数来为被RCU保护的指针分配一个新的值。

注意,synchronize_rcu():这是RCU的核心所在,它挂起写者,等待读者都退出后释放老的数据。

增加链表项

Linux kernel 中利用 RCU 往链表增加项的源码如下:

1
2
3
4
5
6
7
8
9
10
#define list_next_rcu(list)     (*((struct list_head __rcu **)(&(list)->next)))

static inline void __list_add_rcu(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
new->next = next;
new->prev = prev;
rcu_assign_pointer(list_next_rcu(prev), new);
next->prev = new;
}

list_next_rcu() 函数中的 rcu 是一个供代码分析工具 Sparse 使用的编译选项,规定有 rcu 标签的指针不能直接使用,而需要使用 rcu_dereference() 返回一个受 RCU 保护的指针才能使用。rcu_dereference() 接口的相关知识会在后文介绍,这一节重点关注 rcu_assign_pointer() 接口。首先看一下 rcu_assign_pointer() 的源码:

1
2
3
4
5
#define __rcu_assign_pointer(p, v, space) \
({ \
smp_wmb(); \
(p) = (typeof(*v) __force space *)(v); \
})

上述代码的最终效果是把 v 的值赋值给 p,关键点在于第 3 行的内存屏障。什么是内存屏障(Memory Barrier)呢?CPU 采用流水线技术执行指令时,只保证有内存依赖关系的指令的执行顺序,例如 p = v; a = *p;,由于第 2 条指令访问的指针 p 所指向的内存依赖于第 1 条指令,因此 CPU 会保证第 1 条指令在第 2 条指令执行前执行完毕。但对于没有内存依赖的指令,例如上述 __list_add_rcu() 接口中,假如把第 8 行写成 prev->next = new;,由于这个赋值操作并没涉及到对 new 指针指向的内存的访问,因此认为不依赖于 6,7 行对 new->nextnew->prev 的赋值,CPU 有可能实际运行时会先执行 prev->next = new; 再执行 new->prev = prev;,这就会造成 new 指针(也就是新加入的链表项)还没完成初始化就被加入了链表中,假如这时刚好有一个读者刚好遍历访问到了该新的链表项(因为 RCU 的一个重要特点就是可随意执行读操作),就会访问到一个未完成初始化的链表项!通过设置内存屏障就能解决该问题,它保证了在内存屏障前边的指令一定会先于内存屏障后边的指令被执行。这就保证了被加入到链表中的项,一定是已经完成了初始化的。

最后提醒一下,这里要注意的是,如果可能存在多个线程同时执行添加链表项的操作,添加链表项的操作需要用其他同步机制(如 spin_lock 等)进行保护。

访问链表项

Linux kernel 中访问 RCU 链表项常见的代码模式是:

1
2
3
4
5
rcu_read_lock();
list_for_each_entry_rcu(pos, head, member) {
// do something with `pos`
}
rcu_read_unlock();

这里要讲到的 rcu_read_lock()rcu_read_unlock(),是 RCU “随意读” 的关键,它们的效果是声明了一个读端的临界区(read-side critical sections)。在说读端临界区之前,我们先看看读取链表项的宏函数 list_for_each_entry_rcu。追溯源码,获取一个链表项指针主要调用的是一个名为 rcu_dereference() 的宏函数,而这个宏函数的主要实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
#define __rcu_dereference_check(p, c, space) \
({ \
typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); \
rcu_lockdep_assert(c, "suspicious rcu_dereference_check()" \
" usage"); \
rcu_dereference_sparse(p, space); \
smp_read_barrier_depends(); \
((typeof(*p) __force __kernel *)(_________p1)); \
})
// 第 3 行:声明指针 _p1 = p;
// 第 7 行:smp_read_barrier_depends();
// 第 8 行:返回 _p1;

上述两块代码,实际上可以看作这样一种模式:

1
2
3
4
5
6
7
rcu_read_lock();
p1 = rcu_dereference(p);
if (p1 != NULL) {
// do something with p1, such as:
printk("%d\n", p1->field);
}
rcu_read_unlock();

根据 rcu_dereference() 的实现,最终效果就是把一个指针赋值给另一个,那如果把上述第 2 行的 rcu_dereference() 直接写成 p1 = p 会怎样呢?在一般的处理器架构上是一点问题都没有的。但在 alpha 上,编译器的 value-speculation 优化选项据说可能会“猜测” p1 的值,然后重排指令先取值 p1->field~ 因此 Linux kernel 中,smp_read_barrier_depends() 的实现是架构相关的,arm、x86 等架构上是空实现,alpha 上则加了内存屏障,以保证先获得 p 真正的地址再做解引用。因此上一节 “增加链表项” 中提到的 “__rcu” 编译选项强制检查是否使用 rcu_dereference() 访问受 RCU 保护的数据,实际上是为了让代码拥有更好的可移植性。

现在回到读端临界区的问题上来。多个读端临界区不互斥,即多个读者可同时处于读端临界区中,但一块内存数据一旦能够在读端临界区内被获取到指针引用,这块内存块数据的释放必须等到读端临界区结束,等待读端临界区结束的 Linux kernel API 是synchronize_rcu()。读端临界区的检查是全局的,系统中有任何的代码处于读端临界区,synchronize_rcu() 都会阻塞,知道所有读端临界区结束才会返回。为了直观理解这个问题,举以下的代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* `p` 指向一块受 RCU 保护的共享数据 */

/* reader */
rcu_read_lock();
p1 = rcu_dereference(p);
if (p1 != NULL) {
printk("%d\n", p1->field);
}
rcu_read_unlock();

/* free the memory */
p2 = p;
if (p2 != NULL) {
p = NULL;
synchronize_rcu();
kfree(p2);
}

用以下图示来表示多个读者与内存释放线程的时序关系:

img

上图中,每个读者的方块表示获得 p 的引用(第5行代码)到读端临界区结束的时间周期;t1 表示 p = NULL 的时间;t2 表示 synchronize_rcu() 调用开始的时间;t3 表示 synchronize_rcu() 返回的时间。我们先看 Reader1,2,3,虽然这 3 个读者的结束时间不一样,但都在 t1 前获得了 p 地址的引用。t2 时调用 synchronize_rcu(),这时 Reader1 的读端临界区已结束,但 Reader2,3 还处于读端临界区,因此必须等到 Reader2,3 的读端临界区都结束,也就是 t3,t3 之后,就可以执行 kfree(p2) 释放内存。synchronize_rcu() 阻塞的这一段时间,有个名字,叫做 Grace period。而 Reader4,5,6,无论与 Grace period 的时间关系如何,由于获取引用的时间在 t1 之后,都无法获得 p 指针的引用,因此不会进入 p1 != NULL 的分支。

删除链表项

知道了前边说的 Grace period,理解链表项的删除就很容易了。常见的代码模式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
p = seach_the_entry_to_delete();
list_del_rcu(p->list);
synchronize_rcu();
kfree(p);
/* 其中 list_del_rcu() 的源码如下,把某一项移出链表:*/

/* list.h */
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}

/* rculist.h */
static inline void list_del_rcu(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->prev = LIST_POISON2;
}

根据上一节“访问链表项”的实例,假如一个读者能够从链表中获得我们正打算删除的链表项,则肯定在 synchronize_rcu()之前进入了读端临界区,synchronize_rcu()就会保证读端临界区结束时才会真正释放链表项的内存,而不会释放读者正在访问的链表项。

更新链表项

前文提到,RCU 的更新机制是 “Copy Update”,RCU 链表项的更新也是这种机制,典型代码模式是:

1
2
3
4
5
6
7
p = search_the_entry_to_update();
q = kmalloc(sizeof(*p), GFP_KERNEL);
*q = *p;
q->field = new_value;
list_replace_rcu(&p->list, &q->list);
synchronize_rcu(); // 挂起写者,等待读者全部结束
kfree(p); // 释放老数据

其中第 3,4 行就是复制一份副本,并在副本上完成更新,然后调用 list_replace_rcu() 用新节点替换掉旧节点,最后释放旧节点内存。

list_replace_rcu() 源码如下:

1
2
3
4
5
6
7
8
9
static inline void list_replace_rcu(struct list_head *old,
struct list_head *new)
{
new->next = old->next;
new->prev = old->prev;
rcu_assign_pointer(list_next_rcu(new->prev), new);
new->next->prev = new;
old->prev = LIST_POISON2;
}

内存屏障与优化屏障

我们都知道,带有优化的编译器,会尝试重新排序汇编指令,以提高程序的执行速度。但是,当在处理同步问题的时候,重新排序的指令应该被避免。因为重新排序可能会打乱我们之前想要的同步效果。其实,所有的同步原语都可以充当优化和内存屏障。

优化屏障

编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于CPU的并行执行。然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器的重排序优化操作,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。

Linux用宏barrier实现优化屏障,gcc编译器的优化屏障宏定义列出如下(在include/linux/compiler-gcc.h中):

1
#define barrier() __asm__ __volatile__("": : :"memory")

上述定义中,“asm”表示插入了汇编语言程序,“volatile”表示阻止编译器对该值进行优化,确保变量使用了用户定义的精确地址,而不是装有同一信息的一些别名。“memory”表示指令修改了内存单元。

内存屏障

软件可通过读写屏障强制内存访问次序。读写屏障像一堵墙,所有在设置读写屏障之前发起的内存访问,必须先于在设置屏障之后发起的内存访问之前完成,确保内存访问按程序的顺序完成。

读写屏障通过处理器构架的特殊指令mfence(内存屏障)、lfence(读屏障)和sfence(写屏障)完成。另外,在x86-64处理器中,对硬件进行操作的汇编语言指令是“串行的”,也具有内存屏障的作用,如:对I/O端口进行操作的所有指令、带lock前缀的指令以及写控制寄存器、系统寄存器或调试寄存器的所有指令(如:cli和sti)。

内存屏障可用于多处理器和单处理器系统,如果仅用于多处理器系统,就使用smp_xxx函数,在单处理器系统上,它们什么都不要。

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

适合于多处理器和单处理器的内存屏障宏定义列出如下(在include/asm-x86/system.h中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef CONFIG_X86_32
/*
* 指令“lock; addl $0,0(%%esp)”表示加锁,把0加到栈顶的内存单元,该指令操作本身无意义,但这些指令起到内存屏障的作用,
* 让前面的 指令执行完成。具有XMM2特征的CPU已有内存屏障指令,就直接使用该指令
*/
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define mb() asm volatile("mfence":::"memory")
#define rmb() asm volatile("lfence":::"memory")
#define wmb() asm volatile("sfence" ::: "memory")
#endif

/*刷新后面的读所依赖的所有挂起读操作,在x86-64构架上不需要*/
#define read_barrier_depends() do { } while (0)

宏定义ead_barrier_depends()刷新后面的读所依赖的所有挂起读操作,后面的读操作依赖于正处理的读操作返回的数据。在x86-64构架上不需要此宏。它表明:在此屏障之前,没有来自内存区域数据所依赖的读曾经重排序。所有的读操作处理此原语,保证在跟随此原语的任何读操作此原语之前访问内存(但不需要其他CPU的cache)。此原语在大多数CPU上有比rmb()更轻的份量。

本地CPU和编译器遵循内存屏障的排序限制,仅内存屏障原语保证排序,即使数据有依赖关系,也不能保证排序。例如:下面代码将强迫排序,因为*q的读操作依赖于p的读操作,并且这两个读操作被read_barrier_depends()分开。在CPU 0和CPU 1上执行的程序语句分别列出如下:

1
2
3
4
5
6
1. CPU 0                                      1. CPU 1
2. b = 2; 2.
3. memory_barrier(); 3.
4. p = &b; 4. q = p;
5. read_barrier_depends();
6. d = *q;

下面的代码没有强制排序,因为在a和b的读操作之间没有依赖关系,因此,在一些CPU上,如:Alpha,y将设置为3,x设置为0。类似这种没有数据依赖关系的读操作,需要排序应使用rmb()。

1
2
3
4
5
6
1. CPU 0                                      1. CPU 1
2. a = 2; 2.
3. memory_barrier(); 3.
4. b = 3; 4. y = b;
5. read_barrier_depends();
6. x = a;

适合于多处理器的内存屏障宏定义列出如下(在include/asm-x86/system.h中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifdef CONFIG_SMP
#define smp_mb() mb()
#ifdef CONFIG_X86_PPRO_FENCE
# define smp_rmb() rmb()
#else
# define smp_rmb() barrier()
#endif
#ifdef CONFIG_X86_OOSTORE
# define smp_wmb() wmb()
#else
# define smp_wmb() barrier()
#endif
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { (void)xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while (0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif

函数rdtsc_barrier用于加内存屏障阻止RDTSC猜测,当在一个定义的代码区域使用读取时间戳计数器(Read Time-Stamp Counter,RDTSC)函数(或者函数get_cycles或vread)时,必须加内存屏障阻止RDTSC猜测。其列出如下:

1
2
3
4
5
6
static inline void rdtsc_barrier(void)
{
alternative(ASM_NOP3, "mfence", X86_FEATURE_MFENCE_RDTSC);
alternative(ASM_NOP3, "lfence", X86_FEATURE_LFENCE_RDTSC);
[...]
}

内核内存布局

具体在内存管理分析。

6-1

多核调度分析

参考链接

多核调度

SMP(Symmetric Multiprocessing,对称多处理)是一种常见的多核处理器架构。它将多个处理器集成到一个计算机系统中,并通过共享系统总线和内存子系统来实现处理器之间的通信。

首先,SMP架构将一组处理器集中在同一个计算机上。这些处理器可以是物理上独立的芯片,也可以是在同一芯片上集成的多个核心。无论是物理上独立的处理器还是集成在同一芯片上的核心,它们都可以同时工作,处理不同的任务。

在SMP架构中,各处理器是对等的,也就是说它们具有相同的权限和地位。每个处理器都可以独立地执行任务,并且可以访问共享的系统总线和内存子系统。这意味着不同的处理器可以同时读取和写入内存,从而实现数据的共享和协同处理。

通过共享系统总线和内存子系统,SMP架构实现了处理器之间的通信和协作。处理器可以通过总线发送和接收数据,从而进行协同工作。例如,一个处理器可以将计算结果存储在内存中,而另一个处理器可以读取该结果并继续后续的计算。

uma

根据处理器的实际物理属性,CPU可以分为超线程和多核。

  • 超线程(SMT,Simultaneous Multithreading):

Linux内核分类位 CONFIG_SCHED_SMT,超线程是一种技术,允许单个物理处理器核心模拟出多个逻辑处理器核心。这意味着在一个物理处理器核心上可以并行执行多个线程,从而提高处理器的利用率和性能。在Linux内核中,针对超线程技术,可以使用CONFIG_SCHED_SMT进行分类和配置。

  • 多核(MC,Multiple Cores):

Linux内核分类位 CONFIG_SCHED_MC,多核处理器是指在一个集成电路芯片上集成了多个独立的物理处理器核心。每个核心都可以独立地执行任务,因此多核处理器可以同时处理多个线程或进程,从而提高系统的并行处理能力和整体性能。在Linux内核中,针对多核处理器,可以使用CONFIG_SCHED_MC进行分类和配置。

Linux内核对CPU管理主要是通过使用bitmap来实现,同时定义了四种状态:possible、online、active、present。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
* 可能状态(possible):表示系统中存在但尚未启用的CPU。这意味着该CPU是硬件上存在的,
* 但由于某些原因(例如节能策略或热插拔功能),暂时没有被激活。
*/
extern struct cpumask __cpu_possible_mask;
/* 在线状态(online):表示CPU已经被启用并且处于可用状态。在线状态的CPU可以处理任务和线程,并参与系统的运行。*/
extern struct cpumask __cpu_online_mask;
/* 活跃状态(active):表示CPU正在执行任务或线程,即处于活动状态。这是指当前正在被利用的CPU核心。 */
extern struct cpumask __cpu_present_mask;
/* 存在状态(present):表示该CPU核心是物理上存在的,即硬件上存在该CPU核心。 */
extern struct cpumask __cpu_active_mask;
// 系统上有多少个可以执行的cpu核心
#define cpu_possible_mask ((const struct cpumask *)&__cpu_possible_mask)
// 系统上有多少个正在运行的cpu核心
#define cpu_online_mask ((const struct cpumask *)&__cpu_online_mask)
// 系统上有多少个具备online条件的cpu核心,有的cpu可能被热拔插
#define cpu_present_mask ((const struct cpumask *)&__cpu_present_mask)
// 系统上有多少个活跃的cpu核心
#define cpu_active_mask ((const struct cpumask *)&__cpu_active_mask)

在Linux内核中,为了实现对多核处理器的调度和管理,将同一个级别的CPU核心归纳为一个调度组(scheduling group),然后将同一个级别的调度组组合成一个调度域(scheduling domain)

调度组是一组具有相似特性的CPU核心。比如,它们可能位于同一个物理芯片上、共享同一个高速缓存等。通过将这些核心组织在一起,内核可以更有效地进行资源分配和任务调度。调度组通常由内核自动创建,并根据硬件拓扑关系进行组织。

调度域是一组调度组的集合,它们具有相同的调度策略和目标。调度域用于确定任务在系统中的调度范围和选择可用的CPU核心。通过将调度组组合在一起,内核可以更好地控制任务的调度和负载均衡。

调度域的几个常见级别包括:

  1. CPU级别调度域:表示同一个物理芯片上的多个调度组。这个级别的调度域被称为cpu_domain。
  2. NUMA级别调度域:表示非一致性内存访问(NUMA)架构中的不同内存节点。这个级别的调度域被称为node_domain。
  3. 系统级别调度域:表示整个系统中的所有调度组,也可以包括多个NUMA节点。这个级别的调度域被称为sched_domain。

调度域和调度组

调度域(scheduling domain)和调度组(scheduling group)是Linux内核中用于管理多核处理器调度的重要概念。

  1. 调度组:Linux内核将同一个级别的CPU核心归纳为一个调度组。调度组是一组具有相似特性的CPU核心,它们通常位于同一个物理芯片上,或者共享同一个高速缓存。通过将这些核心组织在一起,内核可以更好地进行资源分配和任务调度。每个调度组都有自己的调度队列,内核根据调度策略从不同的调度组中选择适当的CPU核心来运行任务。
  2. 调度域:调度域是一组调度组的集合,它们具有相同的调度策略和目标。调度域用于确定任务在系统中的调度范围和选择可用的CPU核心。调度域的层次结构从低到高包括:硬件线程调度域、核调度域、处理器调度域和NUMA节点调度域。每个调度域都负责管理和调度其下一级调度域中的任务。通过将调度组组合在一起形成调度域,内核可以更好地控制任务的调度和负载均衡。

通常来说,在多核处理器中,处理器拓扑结构是由多个层次结构组成的。在最底层是核心层,一个处理器可以包含多个核心,每个核心有独立的一级缓存,所有核心共享二级缓存。在核心层之上是硬件线程层,也称为虚拟处理器或逻辑处理器,一个核心可以包含多个硬件线程,这些线程共享一级缓存和二级缓存。在硬件线程层之上是处理器层,一个处理器由多个核心和硬件线程组成,处理器共享内存和I/O资源。最高层是NUMA层,它由多个处理器组成,每个处理器可以包含多个处理器层,每个处理器层可以包含多个核心和硬件线程。

处理器拓扑

处理器拓扑结构是指处理器内部各个组件之间的连接关系和层次结构。在多核处理器中,常见的两种拓扑结构是NUMA(Non-Uniform Memory Access)和SMP(Symmetric Multiprocessing)。

  1. NUMA结构:在NUMA结构中,处理器由多个物理节点组成,每个节点包含一部分处理器核心、内存和I/O设备。不同节点之间的访问延迟和带宽不同,因此是“非均匀内存访问”结构。在NUMA结构下,任务调度算法需要考虑到任务在不同节点之间的迁移和负载均衡,以优化系统性能。
  2. SMP结构:在SMP结构中,所有处理器核心都共享同一个物理地址空间,内存访问延迟和带宽相同,因此是“对称多处理”结构。在SMP结构下,任务调度算法可以采用简单的轮转或抢占式算法来实现任务的负载均衡。

通常来说,在多核处理器中,处理器拓扑结构是由多个层次结构组成的。在最底层是核心层,一个处理器可以包含多个核心,每个核心有独立的一级缓存,所有核心共享二级缓存。在核心层之上是硬件线程层,也称为虚拟处理器或逻辑处理器,一个核心可以包含多个硬件线程,这些线程共享一级缓存和二级缓存。在硬件线程层之上是处理器层,一个处理器由多个核心和硬件线程组成,处理器共享内存和I/O资源。最高层是NUMA层,它由多个处理器组成,每个处理器可以包含多个处理器层,每个处理器层可以包含多个核心和硬件线程。

SMP系统启动流程:

  1. 引导处理器(BP)先完成自身的初始化,然后从start_kernel()调用smp_init()进行SMP结构初始化。
  2. smp_init()的主体是 smp_prepare_cpus(),它会为每个处理器核心分配并初始化一个cpu结构体,并设置相应的标志位。
  3. BP使用smp_callin_map[]表格记录每个处理器核心的状态,并向每个AP发送一个初始化IPI(Inter-Processor Interrupt),通知AP进入start_secondary()函数进行进一步初始化。
  4. AP接收到IPI后,会执行trampoline.S中的一段跳板程序,在跳板程序中会将AP的状态设置为SMP_CALLIN,并调用startup_32()函数完成一些基本的初始化工作。
  5. 在start_secondary()函数中,AP会进行进一步的初始化,并等待全局变量smp_commenced变为1。这个变量是在BP完成所有AP的启动后被设置为1的。
  6. BP调用smp_cpus_done()函数等待所有AP完成初始化,并最终调用smp_commence()函数发出起跑命令。
  7. 每个CPU进入cpu_idle()函数,等待调度。在这个函数中,处理器核心会处于空闲状态,并等待操作系统调度器分配任务给它。一旦有任务需要执行,处理器核心就会被唤醒并开始执行任务。

内核数据结构

参考链接

Linux内核链表实现

单向链表和双向链表在实际使用中有一些局限性,如数据区必须是固定数据,而实际需求是多种多样的。这种方法无法构建一套通用的链表,因为每个不同的数据区需要一套链表。为此,Linux内核把所有链表操作方法的共同部分提取出来,把不同的部分留给代码编程者自己去处理。Linux内核实现了一套纯链表的封装,链表节点数据结构只有指针区而没有数据区,另外还封装了各种操作函数,如创建节点函数、插入节点函数、删除节点函数、遍历节点函数等。

Linux内核链表使用struct list_head数据结构来描述。

1
2
3
4
5
<include/linux/types.h>

struct list_head {
struct list_head *next, *prev;
};

struct list_head数据结构不包含链表节点的数据区,通常是嵌入其他数据结构,如struct page数据结构中嵌入了一个lru链表节点,通常是把page数据结构挂入LRU链表。

1
2
3
4
5
6
7
<include/linux/mm_types.h>

struct page {
...
struct list_head lru;
...
}

链表头的初始化有两种方法,一种是静态初始化,另一种动态初始化。把next和prev指针都初始化并指向自己,这样便初始化了一个带头节点的空链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<include/linux/list.h>

/*静态初始化*/
#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)

/*动态初始化*/
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}

添加节点到一个链表中,内核提供了几个接口函数,如list_add()是把一个节点添加到表头,list_add_tail()是插入表尾。

1
2
3
4
<include/linux/list.h>

void list_add(struct list_head *new, struct list_head *head)
list_add_tail(struct list_head *new, struct list_head *head)

遍历节点的接口函数。

1
2
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)

这个宏只是遍历一个一个节点的当前位置,那么如何获取节点本身的数据结构呢?这里还需要使用list_entry()宏。

1
2
3
4
5
6
7
8
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
container_of()宏的定义在kernel.h头文件中。
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

其中offsetof()宏是通过把0地址转换为type类型的指针,然后去获取该结构体中member成员的指针,也就是获取了member在type结构体中的偏移量。最后用指针ptr减去offset,就得到type结构体的真实地址了。

下面是遍历链表的一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<drivers/block/osdblk.c>

static ssize_t class_osdblk_list(struct class *c,
struct class_attribute *attr,
char *data)
{
int n = 0;
struct list_head *tmp;

list_for_each(tmp, &osdblkdev_list) {
struct osdblk_device *osdev;

osdev = list_entry(tmp, struct osdblk_device, node);

n += sprintf(data+n, "%d %d %llu %llu %s\n",
osdev->id,
osdev->major,
osdev->obj.partition,
osdev->obj.id,
osdev->osd_path);
}
return n;
}

红黑树

红黑树(Red Black Tree)被广泛应用在内核的内存管理和进程调度中,用于将排序的元素组织到树中。红黑树被广泛应用在计算机科学的各个领域中,它在速度和实现复杂度之间提供一个很好的平衡。

红黑树是具有以下特征的二叉树。

 每个节点或红或黑。

  • 每个叶节点是黑色的。
  • 如果结点都是红色,那么两个子结点都是黑色。
  • 从一个内部结点到叶结点的简单路径上,对所有叶节点来说,黑色结点的数目都是相同的。

红黑树的一个优点是,所有重要的操作(例如插入、删除、搜索)都可以在O(log n)时间内完成,n为树中元素的数目。经典的算法教科书都会讲解红黑树的实现,这里只是列出一个内核中使用红黑树的例子,供读者在实际的驱动和内核编程中参考。这个例子可以在内核代码的documentation/Rbtree.txt文件中找到。

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
#include <linux/init.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/rbtree.h>

MODULE_AUTHOR("figo.zhang");
MODULE_DESCRIPTION(" ");
MODULE_LICENSE("GPL");

struct mytype {
struct rb_node node;
int key;
};

/*红黑树根节点*/
struct rb_root mytree = RB_ROOT;
/*根据key来查找节点*/
struct mytype *my_search(struct rb_root *root, int new)
{
struct rb_node *node = root->rb_node;

while (node) {
struct mytype *data = container_of(node, struct mytype, node);

if (data->key > new)
node = node->rb_left;
else if (data->key < new)
node = node->rb_right;
else
return data;
}
return NULL;
}

/*插入一个元素到红黑树中*/
int my_insert(struct rb_root *root, struct mytype *data)
{
struct rb_node **new = &(root->rb_node), *parent=NULL;

/* 寻找可以添加新节点的地方 */
while (*new) {
struct mytype *this = container_of(*new, struct mytype, node);

parent = *new;
if (this->key > data->key)
new = &((*new)->rb_left);
else if (this->key < data->key) {
new = &((*new)->rb_right);
} else
return -1;
}

/* 添加一个新节点 */
rb_link_node(&data->node, parent, new);
rb_insert_color(&data->node, root);

return 0;
}

static int __init my_init(void)
{
int i;
struct mytype *data;
struct rb_node *node;

/*插入元素*/
for (i =0; i < 20; i+=2) {
data = kmalloc(sizeof(struct mytype), GFP_KERNEL);
data->key = i;
my_insert(&mytree, data);
}

/*遍历红黑树,打印所有节点的key值*/
for (node = rb_first(&mytree); node; node = rb_next(node))
printk("key=%d\n", rb_entry(node, struct mytype, node)->key);

return 0;
}

static void __exit my_exit(void)
{
struct mytype *data;
struct rb_node *node;
for (node = rb_first(&mytree); node; node = rb_next(node)) {
data = rb_entry(node, struct mytype, node);
if (data) {
rb_erase(&data->node, &mytree);
kfree(data);
}
}
}
module_init(my_init);
module_exit(my_exit);

mytree 是红黑树的根节点,my_insert() 实现插入一个元素到红黑树中,my_search() 根据key来查找节点。内核大量使用红黑树,如虚拟地址空间 VMA 的管理。

进程管理常用API

参考链接

struct 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
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
struct pid_namespace {
struct kref kref; //指向pid_namespace的引用个数
struct pidmap pidmap[PIDMAP_ENTRIES]; //分配pid的位图
struct rcu_head rcu;
int last_pid;
unsigned int nr_hashed;
struct task_struct *child_reaper; /* 托管进程,如果父进程先于子进程退出,托管进程会对孤儿进程调用wait4
* 每个命名空间都具有的进程,发挥的作用相当于全局的init进程
*/
struct kmem_cache *pid_cachep; //分配pid的slab地址
unsigned int level; //当前命名空间的级别,父进程 level = n,则子进程 level = n+1
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 work_struct proc_work;
kgid_t pid_gid;
int hide_pid;
int reboot; /* group exit code if this pidns was rebooted */
struct ns_common ns;
};

enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};

struct pid
{
atomic_t count; // 该pid被不同task_struct引用的次数(不同的命名空间,可以存在相同的pid)
unsigned int level; // 这个pid结构体的深度, root level = 0, root的子空间为1
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX]; // 指向与该pid相连的task
struct rcu_head rcu;
struct upid numbers[1]; /* 变长数组, 存储upid结构, 数组大小为 level+1
* numbers[0], numbers[1],..., numbers[level]
* 由于同一个pid从下往上会被映射到多个 namespace 中
* 这里会保存每一个 namespace 下的 upid 信息
*/
};

/*
* struct upid is used to get the id of the struct pid, as it is
* seen in particular namespace. Later the struct pid is found with
* find_pid_ns() using the int nr and struct pid_namespace *ns.
*/
struct upid {
/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
int nr; // PID具体的值
struct pid_namespace *ns; // 指向命名空间的指针
struct hlist_node pid_chain; /* pid哈希列表(pid_hash)中的节点,用于快速通过nr和ns查找到upid
* 在alloc_pid 时将该节点添加到哈希列表中
*/
};

find_get_pid()

根据进程编号,获取进程描述符。

1
2
3
4
5
6
7
8
9
10
11
struct pid *find_get_pid(pid_t nr) // nr 进程编号
{
struct pid *pid;

rcu_read_lock(); // RCU读加锁
pid = get_pid(find_vpid(nr)); // 调用 get_pid() 增加引用计数
rcu_read_unlock();// RCU读解锁

return pid;
}
EXPORT_SYMBOL_GPL(find_get_pid);

find_vpid()

1
2
3
4
5
struct pid *find_vpid(int nr)
{
return find_pid_ns(nr, task_active_pid_ns(current));
}
EXPORT_SYMBOL_GPL(find_vpid);

get_pid()

1
2
3
4
5
6
static inline struct pid *get_pid(struct pid *pid)
{
if (pid)
refcount_inc(&pid->count);
return pid;
}

pid_task()

获取任务描述符相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 参数pid: pid类型指针,存储进程描述符相关信息
// 参数type: pid_type类型变量
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result = NULL;
if (pid) {
struct hlist_node *first;
first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
lockdep_tasklist_lock_is_held());
if (first)
result = hlist_entry(first, struct task_struct, pid_links[(type)]);
}
return result;
}
EXPORT_SYMBOL(pid_task);

pid_nr()

获取进程全局进程号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* the helpers to get the pid's id seen from different namespaces
*
* pid_nr() : global id, i.e. the id seen from the init namespace;
* pid_vnr() : virtual id, i.e. the id seen from the pid namespace of
* current.
* pid_nr_ns() : id seen from the ns specified.
*
* see also task_xid_nr() etc in include/linux/sched.h
*/

static inline pid_t pid_nr(struct pid *pid)
{
pid_t nr = 0;
if (pid)
nr = pid->numbers[0].nr;
return nr;
}

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns);
pid_t pid_vnr(struct pid *pid);

__task_pid_nr_ns()

获取进程编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type,
struct pid_namespace *ns)
{
pid_t nr = 0;

rcu_read_lock();
if (!ns)
ns = task_active_pid_ns(current);
if (likely(pid_alive(task)))
nr = pid_nr_ns(rcu_dereference(*task_pid_ptr(task, type)), ns);
rcu_read_unlock();

return nr;
}
EXPORT_SYMBOL(__task_pid_nr_ns);

示例

pidtest.c

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
#include<linux/sched.h>
#include<linux/pid.h>
#include<linux/module.h>

static int __init pidtest_initfunc(void)
{
struct pid *kernelpid = find_get_pid(current->pid);
printk("count ==> %d\n", kernelpid->count);
printk("level ==> %d\n", kernelpid->level);
printk("numbers ==> %d\n", kernelpid->numbers[kernelpid->level].nr);

int iNr = pid_nr(kernelpid);
printk("nr ==> %d\n", iNr);
printk("tgid ==> %d\n", current->tgid);

struct task_struct *ttask = pid_task(kernelpid, PIDTYPE_PID);
printk("state ==> %ld\n", ttask->state);
printk("pid ==> %d\n", ttask->pid);

pid_t rest = __task_pid_nr_ns(ttask, PIDTYPE_PID, kernelpid->numbers[kernelpid->level].ns);
printk("rest ==> %d", rest);

return 0;
}

static void __exit pidtest_exitfunc(void)
{
printk("exit...");
}

// “核心内核”(大概包括frob_task_by_pid_hard及其同类)中的frob_task_by_pid_hard是GPL的,需要将模块的许可证声明为GPL。
MODULE_LICENSE("GPL");
module_init(pidtest_initfunc);
module_exit(pidtest_exitfunc);

makefile

1
2
3
4
5
6
7
8
9
10
obj-m:=pidtest.o
CONFIG_MODULE_SIG=n
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)

all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
rm -rf *.mk .tmp_versions Module.symvers *.mod.c *.o *.ko .*.cmd Module.markers modules.order *.a *.mod

dmesg -c

1
2
3
4
5
6
7
8
root@kernel:~/pidtest# dmesg -c
[11762.163090] count ==> 3
[11762.163090] level ==> 0
[11762.163091] numbers ==> 40250
[11762.163091] nr ==> 40250
[11762.163091] tgid ==> 40250
[11762.163091] state ==> 0
[11762.163091] pid ==> 40250

进程调度常用API

kthread_create_on_node()

指定存储节点创建新内核线程。

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
// 参数 int (*threadfn) (void *data) 是一个函数指针,即它是一个函数,此函数是此进程执行时执行的函数,此函数返回值为int型,
// 参数是一个void型指针。
// 参数void * data是一个void型指针,是传递给第一个参数所代表函数的参数,即进程执行时函数的参数。
// 参数node为存储节点编号,内核将内存区域进程编号,如果指定编号,则创建的新进程在指定的内存区域,如果不指定,设置为-1,则内核随机
// 选择内存区域。
// namefmt为进程的输出类型名。
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;
}
EXPORT_SYMBOL(kthread_create_on_node);

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;

spin_lock(&kthread_create_lock);
list_add_tail(&create->list, &kthread_create_list);
spin_unlock(&kthread_create_lock);

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.
*/
if (unlikely(wait_for_completion_killable(&done))) {
/*
* If I was SIGKILLed 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 = create->result;
if (!IS_ERR(task)) {
static const struct sched_param param = { .sched_priority = 0 };
char name[TASK_COMM_LEN];

/*
* task is already visible to other tasks, so updating
* COMM must be protected.
*/
vsnprintf(name, sizeof(name), namefmt, args);
set_task_comm(task, name);
/*
* root may have changed our (kthreadd's) priority or CPU mask.
* The kernel thread should not inherit these properties.
*/
sched_setscheduler_nocheck(task, SCHED_NORMAL, &param);
set_cpus_allowed_ptr(task, cpu_all_mask);
}
kfree(create);
return task;
}

wake_up_process()

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
// 返回0:当前进程处于RUNNING状态或唤醒失败。
// 返回1:当前进程不处于RUNNING状态,唤醒成功。
int wake_up_process(struct task_struct *p)
{
return try_to_wake_up(p, TASK_NORMAL, 0);
}

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
unsigned long flags;
int cpu, success = 0;

preempt_disable();
if (p == current) {
/*
* We're waking current, this means 'p->on_rq' and 'task_cpu(p)
* == smp_processor_id()'. Together this means we can special
* case the whole 'p->on_rq && ttwu_remote()' case below
* without taking any locks.
*
* In particular:
* - we rely on Program-Order guarantees for all the ordering,
* - we're serialized against set_special_state() by virtue of
* it disabling IRQs (this allows not taking ->pi_lock).
*/
if (!(p->state & state))
goto out;

success = 1;
cpu = task_cpu(p);
trace_sched_waking(p);
p->state = TASK_RUNNING;
trace_sched_wakeup(p);
goto out;
}

/*
* If we are going to wake up a thread waiting for CONDITION we
* need to ensure that CONDITION=1 done by the caller can not be
* reordered with p->state check below. This pairs with mb() in
* set_current_state() the waiting thread does.
*/
raw_spin_lock_irqsave(&p->pi_lock, flags);
smp_mb__after_spinlock();
if (!(p->state & state))
goto unlock;

trace_sched_waking(p);

/* We're going to change ->state: */
success = 1;
cpu = task_cpu(p);

/*
* Ensure we load p->on_rq _after_ p->state, otherwise it would
* be possible to, falsely, observe p->on_rq == 0 and get stuck
* in smp_cond_load_acquire() below.
*
* sched_ttwu_pending() try_to_wake_up()
* STORE p->on_rq = 1 LOAD p->state
* UNLOCK rq->lock
*
* __schedule() (switch to task 'p')
* LOCK rq->lock smp_rmb();
* smp_mb__after_spinlock();
* UNLOCK rq->lock
*
* [task p]
* STORE p->state = UNINTERRUPTIBLE LOAD p->on_rq
*
* Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
* __schedule(). See the comment for smp_mb__after_spinlock().
*/
smp_rmb();
if (p->on_rq && ttwu_remote(p, wake_flags))
goto unlock;

#ifdef CONFIG_SMP
/*
* Ensure we load p->on_cpu _after_ p->on_rq, otherwise it would be
* possible to, falsely, observe p->on_cpu == 0.
*
* One must be running (->on_cpu == 1) in order to remove oneself
* from the runqueue.
*
* __schedule() (switch to task 'p') try_to_wake_up()
* STORE p->on_cpu = 1 LOAD p->on_rq
* UNLOCK rq->lock
*
* __schedule() (put 'p' to sleep)
* LOCK rq->lock smp_rmb();
* smp_mb__after_spinlock();
* STORE p->on_rq = 0 LOAD p->on_cpu
*
* Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
* __schedule(). See the comment for smp_mb__after_spinlock().
*/
smp_rmb();

/*
* If the owning (remote) CPU is still in the middle of schedule() with
* this task as prev, wait until its done referencing the task.
*
* Pairs with the smp_store_release() in finish_task().
*
* This ensures that tasks getting woken will be fully ordered against
* their previous state and preserve Program Order.
*/
smp_cond_load_acquire(&p->on_cpu, !VAL);

p->sched_contributes_to_load = !!task_contributes_to_load(p);
p->state = TASK_WAKING;

if (p->in_iowait) {
delayacct_blkio_end(p);
atomic_dec(&task_rq(p)->nr_iowait);
}

cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);
if (task_cpu(p) != cpu) {
wake_flags |= WF_MIGRATED;
psi_ttwu_dequeue(p);
set_task_cpu(p, cpu);
}

#else /* CONFIG_SMP */

if (p->in_iowait) {
delayacct_blkio_end(p);
atomic_dec(&task_rq(p)->nr_iowait);
}

#endif /* CONFIG_SMP */

ttwu_queue(p, cpu, wake_flags);
unlock:
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
out:
if (success)
ttwu_stat(p, cpu, wake_flags);
preempt_enable();

return success;
}

示例

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
#include<linux/sched.h>
#include<linux/pid.h>
#include<linux/module.h>
#include<linux/wait.h>
#include<linux/kthread.h>
#include<linux/delay.h>
#include<linux/list.h>

struct task_struct *pts_thread = NULL;

int test_func(void* argv)
{
int iData = -1;
printk("test_func pid ==> %d\n", current->pid);

iData = wake_up_process(pts_thread);
printk("the state of pts_thread after wake_up_process ==> %ld\n", pts_thread->state);
printk("the res of the wake_up_process ==> %d", iData);

return 0;
}

static int __init test_init(void)
{
int result_data = -1;
char cName[] = "kthread_test.c%s";
struct task_struct *pResult = NULL;
long time_out;

wait_queue_head_t head;
wait_queue_entry_t data;

pResult = kthread_create_on_node(test_func, NULL, -1, cName);
printk("new kthread pid ==> %d\n", pResult->pid);
printk("init pid ==> %d\n", current->pid);

init_waitqueue_head(&head);
init_waitqueue_entry(&data, current);
add_wait_queue(&head, &data);
pts_thread = current;

result_data = wake_up_process(pResult);
printk("after wake_up pResult, result_data ==> %d", result_data);
time_out = schedule_timeout_uninterruptible(2000*10);

result_data = wake_up_process(current);
printk("after wake up current, result_data ==> %d", result_data);

printk("time_out ==> %ld", time_out);


return 0;
}

static void __exit test_exit(void)
{
printk("mod-kthread_test exit normal...");
}

MODULE_LICENSE("GPL");
module_init(test_init);
module_exit(test_exit);

task_nice()

获取进程对应nice值。

1
2
3
4
5
// 返回进程的nice值
static inline int task_nice(const struct task_struct *p)
{
return PRIO_TO_NICE((p)->static_prio);
}

复习

1
2
3
#define MAX_NICE	19
#define MIN_NICE -20
#define NICE_WIDTH (MAX_NICE - MIN_NICE + 1)

nice = static_prio-120,其范围是 -20~19,nice 值越小,优先级越高。

set_user_nice()

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
void set_user_nice(struct task_struct *p, long nice)
{
bool queued, running;
int old_prio;
struct rq_flags rf;
struct rq *rq;

if (task_nice(p) == nice || nice < MIN_NICE || nice > MAX_NICE)
return;
/*
* We have to be careful, if called from sys_setpriority(),
* the task might be in the middle of scheduling on another CPU.
*/
rq = task_rq_lock(p, &rf);
update_rq_clock(rq);

/*
* The RT priorities are set via sched_setscheduler(), but we still
* allow the 'normal' nice value to be set - but as expected
* it wont have any effect on scheduling until the task is
* SCHED_DEADLINE, SCHED_FIFO or SCHED_RR:
*/
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
p->static_prio = NICE_TO_PRIO(nice);
goto out_unlock;
}
queued = task_on_rq_queued(p);
running = task_current(rq, p);
if (queued)
dequeue_task(rq, p, DEQUEUE_SAVE | DEQUEUE_NOCLOCK);
if (running)
put_prev_task(rq, p);

p->static_prio = NICE_TO_PRIO(nice);
set_load_weight(p, true);
old_prio = p->prio;
p->prio = effective_prio(p);

if (queued)
enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK);
if (running)
set_next_task(rq, p);

/*
* If the task increased its priority or is running and
* lowered its priority, then reschedule its CPU:
*/
p->sched_class->prio_changed(rq, p, old_prio);

out_unlock:
task_rq_unlock(rq, p, &rf);
}
EXPORT_SYMBOL(set_user_nice);

示例

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
#include<linux/sched.h>
#include<linux/pid.h>
#include<linux/module.h>
#include<linux/wait.h>
#include<linux/kthread.h>
#include<linux/delay.h>
#include<linux/list.h>

int test_func(void* argv)
{
printk("init test_func\n");
printk("current static_prio ==> %d\n", current->static_prio);
printk("task_nice(current) ==> %d\n", task_nice(current));
printk("current_pid ==> %d\n", current->pid);
printk("exit test_func\n");
return 0;
}

static int __init test_init(void)
{
printk("init func\n");
struct task_struct *pts = NULL;
int iNice;

printk("current->pid ==> %d\n", current->pid);

pts = kthread_create_on_node(test_func,NULL,-1,"kthread_test");
wake_up_process(pts);
iNice = task_nice(pts);

printk("pts->pid ==> %d\n", pts->pid);
printk("pts->static_prio ==> %d\n", pts->static_prio);
printk("task_nice(pts) ==> %d\n", iNice);
printk("pts->prio ==> %d\n", pts->prio);

set_user_nice(pts, -20);
printk("pts->pid ==> %d\n", pts->pid);
printk("pts->static_prio ==> %d\n", pts->static_prio);
printk("task_nice(pts) ==> %d\n", iNice);
printk("pts->prio ==> %d\n", pts->prio);

printk("exit func\n");
return 0;
}

static void __exit test_exit(void)
{
printk("mod-kthread_test exit...");
}

MODULE_LICENSE("GPL");
module_init(test_init);
module_exit(test_exit);

complete_all()

主要用于唤醒等待队列中所有睡眠进程。唤醒进程不是同步操作。

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
/**
* complete_all: - signals all threads waiting on this completion
* @x: holds the state of this particular completion
*
* This will wake up all threads waiting on this particular completion event.
*
* If this function wakes up a task, it executes a full memory barrier before
* accessing the task state.
*
* Since complete_all() sets the completion of @x permanently to done
* to allow multiple waiters to finish, a call to reinit_completion()
* must be used on @x if @x is to be used again. The code must make
* sure that all waiters have woken and finished before reinitializing
* @x. Also note that the function completion_done() can not be used
* to know if there are still waiters after complete_all() has been called.
*/
struct completion {
unsigned int done;
wait_queue_head_t wait;
};

void complete_all(struct completion *x)
{
unsigned long flags;

spin_lock_irqsave(&x->wait.lock, flags);
x->done = UINT_MAX;
__wake_up_locked(&x->wait, TASK_NORMAL, 0);
spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete_all);

示例

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
#include<linux/sched.h>
#include<linux/pid.h>
#include<linux/module.h>
#include<linux/wait.h>
#include<linux/kthread.h>
#include<linux/wait.h>
#include<linux/list.h>
#include<linux/delay.h>

static struct task_struct *pthread;
static struct completion cp;

int test_func(void* argv)
{
printk("parent thread state ==> %ld\n", pthread->state);
printk("parent thread pid ==> %d\n", pthread->pid);
printk("kthread pid ==> %d\n", current->pid);
printk("kthread done ==> %d\n",cp.done);

complete_all(&cp);

printk("parent thread pid ==> %d\n", pthread->pid);
printk("kthread done ==> %d\n",cp.done);
printk("parent thread state ==> %ld\n", pthread->state);

return 0;
}

static int __init test_init(void)
{
struct task_struct *pts = NULL;
wait_queue_entry_t data;
long lefttime;

pthread = current;
pts = kthread_create_on_node(test_func, NULL, -1, "kthread_test");
wake_up_process(pts); // wake the new kthread up

init_completion(&cp);
init_wait_entry(&data, current);
add_wait_queue(&(cp.wait), &data);

lefttime = schedule_timeout_uninterruptible(10000);
printk("timeout ==> %ld\n", lefttime);


return 0;
}

static void __exit test_exit(void)
{
printk("mod-kthread_test exit...");
}

MODULE_LICENSE("GPL");
module_init(test_init);
module_exit(test_exit);

__wake_up_sync_key()

此函数用于唤醒等待队列中处于特定状态的进程,此特定状态是此函数的第二个参数mode定义的。当进程的状态满足此特定状态时就有可能被唤醒,获得CPU资源,从而被调度执行。此函数唤醒的进程不会改变进程之前所在的CPU,不会引起额外的CPU的抢占,并且可以同步唤醒进程。

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
/**
* __wake_up_sync_key - wake up threads blocked on a waitqueue.
* @wq_head: the waitqueue
* @mode: which threads
* @key: opaque value to be passed to wakeup targets
*
* The sync wakeup differs that the waker knows that it will schedule
* away soon, so while the target thread will be woken up, it will not
* be migrated to another CPU - ie. the two threads are 'synchronized'
* with each other. This can prevent needless bouncing between CPUs.
*
* On UP it can prevent extra preemption.
*
* If this function wakes up a task, it executes a full memory barrier before
* accessing the task state.
*/
void __wake_up_sync_key(struct wait_queue_head *wq_head, unsigned int mode,
void *key)
{
if (unlikely(!wq_head))
return;

__wake_up_common_lock(wq_head, mode, 1, WF_SYNC, key);
}
EXPORT_SYMBOL_GPL(__wake_up_sync_key);

示例

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
/*头文件引用*/
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/wait.h>
#include <linux/list.h>
#include <linux/kthread.h>
MODULE_LICENSE("GPL");

/*全局变量定义*/
static wait_queue_head_t head; //等待队列头元素
struct task_struct * old_thread; //保存进程描述符信息

int my_function(void * argc)
{
printk("in the kernel thread function! \n");
printk("the current pid is:%d\n", current->pid); //显示当前进程的PID值
/*显示父进程的状态*/
printk("the state of the init funcation is :%ld\n", old_thread->state);
__wake_up_sync(&head, TASK_NEW,0); //调用函数唤醒等待队列中的进程

// 显示函数调用之后的父进程的状态
printk("the state of the init function after __wake_up_sync is :%ld\n", old_thread->state);
printk("out the kernel thread function\n");
return 0;
}

static int __init __wake_up_sync_init(void)
{
char namefrm[]="__wake_up_sync.c%s"; //线程的输出类型名,在此程序中无影响
long time_out; //保存schedule_timeout_uninterruptible( )的返回结果
struct task_struct * result; //保存新进程的信息
wait_queue_t data; //等待队列元素
printk("into __wake_up_sync_init.\n");
result=kthread_create_on_node(my_function, NULL, -1, namefrm); // 创建新进程
printk("the pid of the new thread is:%d\n", result->pid); //显示新线程的PID值
printk("the current pid is:%d\n", current->pid); //显示当前进程的PID值

init_waitqueue_head(&head); //初始化等待队列头元素
init_waitqueue_entry(&data, current); //用当前进程初始化等待队列中的一个元素
add_wait_queue(&head, &data); //将等待队列元素加入等待队列中
old_thread=current; //记录当前进程的信息
wake_up_process(result); //唤醒新创建的线程
time_out=schedule_timeout_uninterruptible(1000*10); //让当前进程进入睡眠状态,可以改小一点

//输出schedule_timeout_uninterruptible( )返回结果
printk("the schedule timeout is:%ld\n", time_out);
printk("out __wake_up_sync_init.\n");
return 0;
}

static void __exit __wake_up_sync_exit(void)
{
printk("Goodbye __wake_up_sync\n");
}

module_init(__wake_up_sync_init);
module_exit(__wake_up_sync_exit);
  • Title: Linux内核分析之进程
  • Author: 韩乔落
  • Created at : 2024-01-29 22:11:49
  • Updated at : 2024-04-28 19:44:06
  • Link: https://jelasin.github.io/2024/01/29/Linux内核分析之进程/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments