explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Linux下的 task_struct

参考:

  1. 《Linux 内核设计与实现》
kernel version arch
v5.4.x lts arm32

再来往细节深入一下 task_struct,由于 kernel 不明确区分进程和线程,所以统一使用任务来称呼。

分配 task_struct(任务描述符)

由于 task_struct(声明于 include/linux/sched.h ,代码使用包含 <linux/sched.h>) 是属于会频繁使用到的数据结构(每创建一个进程或线程,都会为其分配一个 task_struct), 所以内核使用 slab 分配器预先分配多个 task_struct,这样做有以下好处:

  • 由于预先分配了很多 task_struct,内核就在这片内存中使用和释放,避免了内存碎片
  • 由于 task_struct 是预先分配的,所以内核申请该数据结构的速度很快

关于 slab 预分配的 task_struct 使用详情,可以通过 /proc/slabinfo 文件查看

内核在申请一个新的 task_struct 时,会在栈底(栈向下增长)创建一个新的结构 struct thread_info (声明于 arch/xxx/include/asm/thread_info.h ,代码使用包含 <asm/thread_info.h>), 在 thread_info 结构中包含了指向 task_struct 的指针 task,该变量指向了该任务的 task_struct。

  • 之所以放在栈底,是因为代码可以直接根据偏移方便的得出 thread_info
  • task_struct 是通过双向循环链表链接起来的,所以只要找到了头,便可以遍历系统所有的线程

在有了 task_struct 的情况下还使用 thread_info 的原因是: 最开始是将 task_struct 放置于任务的栈底,但是一个 task_struct 占用好几个 kB 的内存,而 kernel 预设的栈大小一般是 8K,这对于栈空间消耗太严重了。 thread_info 相当于一个缩略版的 task_struct,占用的空间小得多,便于节省任务的栈空间。

任务描述符的存放

PID(process identification value) 也是存放于 task_struct 中,类型为 pid_t(实际上是 int 类型)。

PID 的最大值限制位于 <linux/threads.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
/*
* The default limit for the nr of threads is now in
* /proc/sys/kernel/threads-max.
*/

/*
* Maximum supported processors. Setting this smaller saves quite a
* bit of memory. Use nr_cpu_ids instead of this except for static bitmaps.
*/
#ifndef CONFIG_NR_CPUS
/* FIXME: This should be fixed in the arch's Kconfig */
#define CONFIG_NR_CPUS 1
#endif

/* Places which use this should consider cpumask_var_t. */
//! 通过此宏可以得到当前逻辑 CPU 的数量
#define NR_CPUS CONFIG_NR_CPUS

#define MIN_THREADS_LEFT_FOR_ROOT 4

/*
* This controls the default maximum pid allocated to a process
*/
//! 如果没有配置 CONFIG_BASE_SMALL,那么默认最大值就是 4096,否则是 32768
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

/*
* A maximum of 4 million PIDs should be enough for a while.
* [NOTE: PID/TIDs are limited to 2^29 ~= 500+ million, see futex.h.]
*/
//! 当没有配置 CONFIG_BASE_SMALL 的情况下,如果 long 的长度大于 4,那么极限 PID 可以到 400 多万个
#define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \
(sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))

//! 每个核上可以跑的任务数
/*
* Define a minimum number of pids per cpu. Heuristically based
* on original pid max of 32k for 32 cpus. Also, increase the
* minimum settable value for pid_max on the running system based
* on similar defaults. See kernel/pid.c:pidmap_init() for details.
*/
#define PIDS_PER_CPU_DEFAULT 1024
#define PIDS_PER_CPU_MIN 8

之前已经学习过:对于 linux 来讲,无论进程还是线程对于 kernel 都是一个 task_struct,只不过线程的 task_struct 相关内存、文件等资源是共享的。 在新建一个线程的时候,其实内核也会为线程分配一个 pid,只不过通过 tgid 来抽象化给了用户,让用户在不同线程下得到的 pid 都相同。

基于以上基础,再来看 proc 文件系统下的限制就明了了:

  • /proc/sys/kernel/pid_max : 整个系统(内核空间和用户空间)可以同时存在的最多的 pid 数量
    • 对于内核来讲,就是系统最多可以创建的 task_struct 的数量
  • /proc/sys/kernel/threads-max : 单个进程可以创建的最多线程的数量
    • 对于内核来讲,就是一个 task_struct 链表上最多可以链接的 task_struct 节点的个数
  • ulimit -u :单个用户可以同时创建的最多进程的数量
    • 对于内核来讲,就是一个 uid 可以对应最多多少个 task_struct

内核对于任务的操作基本上都是通过 task_struct 来执行的,为此内核提供了 current 宏,该宏返回的便是当前被调度到的 task_struct 指针。

对于 ARM32 获取 thread_info 的操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//!一般情况下,当 PAGE_SIZE 为 4096 时,栈的大小则是 8K
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
#define THREAD_START_SP (THREAD_SIZE - 8)
/*
* how to get the current stack pointer in C
*/
register unsigned long current_stack_pointer asm ("sp");

/*
* how to get the thread information struct from C
*/

//! 栈是由高地址向低地址方向增长的,那么当前的栈指针将低位清零后,便得到了栈底的地址
//! 这个地址就正好是 thread_info 的地址
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}

获取到 thread_info 后,便可以获取其 task 指针了:

1
2
3
//! file: include/asm-generic/current.h
#define get_current() (current_thread_info()->task)
#define current get_current()

任务状态

为了不以进程和线程做区分,下面统一以任务代表一个 task_struct。

task_struct 中的 state 描述了当前 task 的状态。

state 的值有以下几种状态:

1
2
3
4
5
6
7
8
9
10
11
/* Used in tsk->state: */
//! 运行,任务正在被执行或在运行队列中等待被执行
#define TASK_RUNNING 0x0000
//! 可中断,任务处于睡眠状态,它可以被等待的条件唤醒,也可以被信号唤醒
#define TASK_INTERRUPTIBLE 0x0001
//! 不可中断,相比可中断状态,它不能被信号唤醒
#define TASK_UNINTERRUPTIBLE 0x0002
//! 任务没有运行,也不能投入运行
#define __TASK_STOPPED 0x0004
//! 被其他任务跟踪的任务
#define __TASK_TRACED 0x0008

在进行驱动编程时,如果需要支持阻塞操作,那么就需要设置和获取当前线程的状态:

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
#define task_is_traced(task)		((task->state & __TASK_TRACED) != 0)

#define task_is_stopped(task) ((task->state & __TASK_STOPPED) != 0)

#define task_is_stopped_or_traced(task) ((task->state & (__TASK_STOPPED | __TASK_TRACED)) != 0)

#define task_contributes_to_load(task) ((task->state & TASK_UNINTERRUPTIBLE) != 0 && \
(task->flags & PF_FROZEN) == 0 && \
(task->state & TASK_NOLOAD) == 0)

//! 不带内存屏障,将任务由睡眠设置为运行一般使用此宏
#define __set_current_state(state_value) \
current->state = (state_value)

//! 带内存屏障,将任务由运行设置为睡眠,一般使用此宏以保证互斥
#define set_current_state(state_value) \
smp_store_mb(current->state, (state_value))

//! 设置为睡眠
for (;;) {
//! 切换状态
set_current_state(TASK_UNINTERRUPTIBLE);
//! 确认需要睡眠
if (!need_sleep)
break;

//! 开始调度其他任务
schedule();
}
//! 任务已经被唤醒,设置为运行状态
__set_current_state(TASK_RUNNING);

任务的上下文

当一个任务调用了系统调用或者触发了某个异常陷入内核空间,此时该任务就处于任务上下文中, 这种情况下,current 宏才是有效的。

  • 如果是中断陷入了内核,此时内核并没有对应相关的任务,这种情况下 current 宏就是无意义的。

任务的家族树

每个 task_struct 都包含一个指向其父任务的 parent 指针,还包含一个指向子任务的 children 链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* Pointers to the (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/

/* Real parent process: */
struct task_struct __rcu *real_parent;

/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;

/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;

通过以下方式便可以遍历一个任务的子任务:

1
2
3
4
5
6
7
8
struct task_struct *task;
struct list_head *list;

list_for_each(list, &current->children)
{
//得到的 task 即为当前任务的一个子任务
task = list_entry(list, struct task_struct, sibling);
}

对于初始任务(1 号进程),在 init/init_task.c 中已经初始化好了 1 号进程,也就是 init_task

使用下面的代码,可以搜寻到当前任务的父任务树:

1
2
3
4
5
6
struct task_struct *task;

for(task = current; task != &init_task; task = task->parent)
{
//...
}

task_struct 中具有一个 tasks 元素,这是一个双向链表,将所有的 task_struct 都链接了起来,所以可以通过下面的方式遍历所有的 task_struct:

1
2
3
4
5
#define next_task(p)                                          \
list_entry_rcu((p)->tasks.next, struct task_struct, tasks)

#define for_each_process(p) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )
Last Updated 2020-10-19 一 22:29.
Render by hexo-renderer-org with Emacs 26.3 (Org mode 9.4)