尝试做成一个系列,相关文章请参考:Linux5.10 代码解析总目录
概述
struct task_struct
是一个长达700+行的结构体。
无论是通用 OS 还是 RTOS,肯定都会有自己的进程描述符。
结构体的开始、结束和 randomized_struct_fields_
标志:
struct task_struct {
struct thread_info thread_info;
volatile long state;
/*
* This begins the randomizable portion of task_struct. Only
* scheduling-critical items should be added above here.
*/
randomized_struct_fields_start
...
...
...
/*
* 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!
*/
};
以
struct thread_info thread_info; // arch/arm64/include/asm/thread_info.h
开头,以
struct thread_struct thread; // arch/arm64/include/asm/processor.h
结尾。
并且struct thread_info thread_info
、volatile long state
和 struct thread_struct thread
相对于头尾的位置是固定的。
即不得擅自修改randomized_struct_fields_start
之前、randomized_struct_fields_end
之后的代码!!!
那么randomized_struct_fields_start
、randomized_struct_fields_end
是什么呢?
这是两个宏定义,不同的编译器,定义有些区别;但是,可以粗略的理解为:
// include/linux/compiler_types.h
#ifndef randomized_struct_fields_start
# define randomized_struct_fields_start
# define randomized_struct_fields_end
#endif
这个也很厉害了,定义了一个空的宏定义,只是为了让人看,提出一个编码约定。
第一次看到struct task_struct
中的randomized_struct_fields_
标志,还有些疑惑:这也不像是个变量定义啊。
然后就想,这是不是个宏定义啊?然后就搜索到了compiler_types.h
文件。
但是,其实并没有在sched.h
中找到compiler_types.h
的包含链。
后来发现,是在 makefile 的编译 cmd 中通过 -include
指定的头文件包含。
所以 include 的依赖关系,不一定在 .h 或者 .c 里体现,还有可能在编译命令中指定。
下文为了便于分析,打乱了struct task_struct
中代码的顺序。
thread_info
// include/linux/sched.h
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;
// 仅适用于 ARM64
// 基于多个原因,thread_info 由常驻内核迁移到 task_struct 中
// 如注释所说,必须放到 task_struct 的起始位置
#endif
void *stack;
...
}
thread_info
:
// arch/arm64/include/asm/thread_info.h
/*
* low level task data that entry.S needs immediate access to.
*/
struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
...
};
ARM64 中,thread_info
的重大变化是,去掉了指向进程描述符的 task 指针。
-
之前,
thread_info
是常驻内核的,和进程的内核栈在一起。
进程的内核栈的作用,暂不分析。
因为内核栈是 8K 对齐的,所以可以通过 sp 找到内核栈的栈底,从而找到thread_info
。
然后根据thread_info
中的task_struct
指针,找到进程描述符。 -
现在,
thread_info
在 task 描述符中。内核栈中已经没有 task 的信息了。
陷入内核时,task 在用户态的栈指针本来是存在 SP_EL0 中的。
陷入内核后,内核会做一些列的上下文保存工作。
陷入内核的如果是 svc 系统调用异常,那么就会在 el0_sync 汇编函数做上下文保存工作。
el0_sync 在内核态,会在栈里开辟一个struct pt_regs
大小的空间。
用户态的 SP_EL0 会被保存到栈里的pt_regs->sp
,然后再将 task_struct 指针放到 SP_EL0 中。
在内核态,获得 current task_struct 的操作:
static __always_inline struct task_struct *get_current(void)
{
unsigned long sp_el0;
asm ("mrs %0, sp_el0" : "=r" (sp_el0));
return (struct task_struct *)sp_el0;
}
#define current get_current()
要知道,在用户态,是不需要访问 task_struct 描述符的;当然,也访问不了。
获得current task_struct
以后,通过current task_struct
中的 *stack 获得内核栈
进程属性相关
// include/linux/sched.h
struct task_struct {
...
/* -1 unrunnable, 0 runnable, >0 stopped: */
volatile long state;
pid_t pid;
pid_t tgid;
/* Per task flags (PF_*), defined further below: */
unsigned int flags;
int exit_state; // 下面 exit 字眼的变量,是用来存放线程的退出状态的
int exit_code;
int exit_signal;
/* The signal sent when the parent dies: */ // 父进程消亡时发出的信号
int pdeath_signal;
/*
* 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]; // 如注释所说,存放进程名,绝对路径
/* Process credentials: */ // 下面 cred 字眼的变量是用来存放认证信息的
/* 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;
/* CPU-specific state of this task: */
struct thread_struct thread;
...
}
这里重点说下 state
、pid
和tgid
、flags
这 4 个变量。
state
如果在网上查询 Linux 内核中进程的状态,网上有很多版本。。。。。。
但是我感觉都不权威,或者人家说的是进程的状态,而不是专指 state 的状态。
此处,我们基于源码进行分析。
/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010
#define EXIT_ZOMBIE 0x0020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED 0x0040
#define TASK_DEAD 0x0080
#define TASK_WAKEKILL 0x0100
#define TASK_WAKING 0x0200
#define TASK_NOLOAD 0x0400
#define TASK_NEW 0x0800
#define TASK_STATE_MAX 0x1000
/* Convenience macros for the sake of set_current_state: */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)
#define TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD)
/* Convenience macros for the sake of wake_up(): */
#define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
感觉很早的版本,state 的值不是 bitmap 来着。
上面的注释已经很清楚了。
EXIT_DEAD
和 EXIT_ZOMBIE
属于进程的状态,但是会赋给 exit_state
关于 TASK_RUNNING
Linux 的进程,运行和就绪都是这一个状态。
应该是 Linux 内核的哲学问题。
Linux 中TASK_RUNNING
状态的进程,分为两个队列,一个是运行队列,一个是就绪队列。
当前运行队列里没进程了,就把之前的就绪队列切成新的运行队列。
它们的 state
都是 TASK_RUNNING
,这样就不用为成百上千个进程依次改 state
值了。
关于 TASK_INTERRUPTIBLE
和 TASK_UNINTERRUPTIBLE
这两个值容易让刚接触的人疑惑。这里解释下:
这两个状态有个前提:都是休眠态。
一个是可中断的,一个是不可中断的;问题是,被什么中断?
被信号中断!
内核向应用进程传递信息的常规途径就是信号,也就是大家熟悉的 kill -9 / kill -15
巴拉巴拉的那些信号。
在 TASK_UNINTERRUPTIBLE
状态下,进程是不接收内核信号的。
所以,当在系统中看到一个进程是 D 状态的时候,如果 kill 不掉这个进程,也就不要奇怪了,系统设计如此。
state
的设置
// include/linux/sched.h
#define set_current_state(state_value) \
smp_store_mb(current->state, (state_value))
// include/asm-generic/barrier.h
#define smp_store_mb(var, value) __smp_store_mb(var, value)
#define __smp_store_mb(var, value) do { WRITE_ONCE(var, value); __smp_mb(); } while (0)
// arch/arm64/include/asm/barrier.h
#define __smp_mb() dmb(ish)
#define dmb(opt) asm volatile("dmb " #opt : : : "memory")
barrier
,大名鼎鼎的内存屏障。
barrier.h
里面就是各种内存一致性相关的处理。
具体到这个宏,最后用到了 dmb 指令。
内存屏障可以确保内存访问、程序执行等行为更符合预期。但是,会损失一部分性能。
ARM64 有三条内存屏障指令,按照屏蔽强度排列:DMB < DSB < ISB
为什么会需要内存屏障,复杂点说,是因为超标量架构和乱序执行。
可以近似的理解,是因为流水线。
屏蔽强度最大的 ISB 指令是最好理解的:可以简单的理解为,让 CPU 放弃流水线,要等当前的指令执行完毕以后,再去预取下一条指令。
ISB 也是最低配(最必要)的指令。在我的记忆里,Cortex-M3 中只有 ISB,没有 DMB、DSB.
pid
和 tgid
pid
和 tgid
这两个概念非常容易被混淆。
为什么呢?因为概念定义和叫法的问题。相对于 UNIX 通用的叫法,Linux 对线程进程的叫法有点不一样。
UNIX 中,一个进程(process)中可以有多个线程(thread);这些 thread 的内存是共享的。
但是,在 Linux 中,只有 process,没有 thread;或者说所有的 thread 都用 task_struct 来表示。这个说法有点矛盾对吧。
要点:Linux 中的每个 task_struct 的 pid 都是唯一的;存在 tgid 相同的 task_struct
在 Linux 中:
- 如果创建的是 UNIX 概念的进程,那么,它的 pid 和 tgid 是一样的
- 如果要创建 UNIX 概念的线程(某个进程的子线程),那么,它的 pid 是唯一的,它的 tgid 与创建它的那个 task_struct 的 tgid 相同
- 使用 POSIX 接口的系统调用 getpid,得到的是 task_struct 的 tgid
- 使用 POSIX 接口的系统调用 gettid,得到的是 task_struct 的 pid
是不是有点乱,不要在意这些小细节,只是名字而已,task 的本质是什么才重要。
要相信,Linux 关于线程的设计,是简洁合理的。
flags
这个一个 bitmap 型的标志位。
// include/linux/sched.h
/*
* Per process flags
*/
#define PF_VCPU 0x00000001 /* I'm a virtual CPU */
#define PF_IDLE 0x00000002 /* I am an IDLE thread */
#define PF_EXITING 0x00000004 /* Getting shut down */
#define PF_IO_WORKER 0x00000010 /* Task is an IO worker */
#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */
#define PF_FORKNOEXEC 0x00000040 /* Forked but didn't exec */
#define PF_MCE_PROCESS 0x00000080 /* Process policy on mce errors */
#define PF_SUPERPRIV 0x00000100 /* Used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* Dumped core */
#define PF_SIGNALED 0x00000400 /* Killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_NPROC_EXCEEDED 0x00001000 /* set_user() noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH 0x00002000 /* If unset the fpu must be initialized before use */
#define PF_USED_ASYNC 0x00004000 /* Used async_schedule*(), used by module init */
#define PF_NOFREEZE 0x00008000 /* This thread should not be frozen */
#define PF_FROZEN 0x00010000 /* Frozen for system suspend */
#define PF_KSWAPD 0x00020000 /* I am kswapd */
#define PF_MEMALLOC_NOFS 0x00040000 /* All allocation requests will inherit GFP_NOFS */
#define PF_MEMALLOC_NOIO 0x00080000 /* All allocation requests will inherit GFP_NOIO */
#define PF_LOCAL_THROTTLE 0x00100000 /* Throttle writes only against the bdi I write to,* I am cleaning dirty pages from some other bdi. */
#define PF_KTHREAD 0x00200000 /* I am a kernel thread */
#define PF_RANDOMIZE 0x00400000 /* Randomize virtual address space */
#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */
#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_mask */
#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */
#define PF_MEMALLOC_NOCMA 0x10000000 /* All allocation request will have _GFP_MOVABLE cleared */
#define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK 0x80000000 /* This thread called freeze_processes() and should not be frozen */
这个 flag 控制着 task 的属性和行为。
进程调度相关
// include/linux/sched.h
struct task_struct {
...
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;
unsigned int policy;
int nr_cpus_allowed;
const cpumask_t *cpus_ptr;
cpumask_t cpus_mask;
struct sched_info sched_info;
struct list_head tasks;
int on_rq;
...
}
这些概念都比较复杂,需要在单独的篇章中详细分析。
内存管理和文件管理相关
// include/linux/sched.h
struct task_struct {
...
struct mm_struct *mm;
struct mm_struct *active_mm;
/* Per-thread vma caching: */
struct vmacache vmacache;
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
...
}
总之,这些变量,是这个进程拥有的资源。内存和打开的文件。
进程间关系相关
// include/linux/sched.h
struct task_struct {
...
/* Real parent process: */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
/*
* 'ptraced' is the list of tasks this task is using ptrace() on.
*
* This includes both natural children and PTRACE_ATTACH targets.
* 'ptrace_entry' is this task's link on the p->parent->ptraced list.
*/
struct list_head ptraced;
struct list_head ptrace_entry;
/* PID/PID hash table linkage. */
struct pid *thread_pid;
struct hlist_node pid_links[PIDTYPE_MAX];
struct list_head thread_group;
struct list_head thread_node;
struct completion *vfork_done;
/* CLONE_CHILD_SETTID: */
int __user *set_child_tid;
/* CLONE_CHILD_CLEARTID: */
int __user *clear_child_tid;
...
}
进程间的关系,主要是一系列链表的管理。
在 Linux 中,struct list_head
这个结构体的用法很使用、很灵活,但是也引入了很大的代码复杂度。
普通的双向链表节点,它某一时刻只能在某一个链表中。
但是 struct list_head
的引入,使得 struct task_struct
可以同时存在于多个链表里。
时间和统计相关
// include/linux/sched.h
struct task_struct {
...
u64 utime;
u64 stime;
u64 gtime;
struct prev_cputime prev_cputime;
/* Context switch counts: */
unsigned long nvcsw;
unsigned long nivcsw;
/* Monotonic time in nsecs: */
u64 start_time;
/* Boot based time in nsecs: */
u64 start_boottime;
...
}
每个 OS 的进程描述符中,都会有统计相关的数据。
这是为调度服务的,也是持续优化调度机制的基础。
信号相关
// include/linux/sched.h
struct task_struct {
...
/* 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;
/* 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;
...
}
信号,内核向用户进程通信的机制。
也需要后续详细分析。