Linux5.10 源码解析 - 进程描述符

尝试做成一个系列,相关文章请参考: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_infovolatile long statestruct thread_struct thread相对于头尾的位置是固定的。
不得擅自修改randomized_struct_fields_start之前、randomized_struct_fields_end之后的代码!!!

那么randomized_struct_fields_startrandomized_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;
	...
}

这里重点说下 statepidtgidflags 这 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_DEADEXIT_ZOMBIE 属于进程的状态,但是会赋给 exit_state

关于 TASK_RUNNING

Linux 的进程,运行和就绪都是这一个状态。
应该是 Linux 内核的哲学问题。
Linux 中TASK_RUNNING状态的进程,分为两个队列,一个是运行队列,一个是就绪队列。
当前运行队列里没进程了,就把之前的就绪队列切成新的运行队列。
它们的 state 都是 TASK_RUNNING,这样就不用为成百上千个进程依次改 state 值了。

关于 TASK_INTERRUPTIBLETASK_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.

pidtgid

pidtgid这两个概念非常容易被混淆。
为什么呢?因为概念定义和叫法的问题。相对于 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;
	...
}

信号,内核向用户进程通信的机制。
也需要后续详细分析。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值