Linux进程数据结构分析

Linux进程结构分析

在 linux 里面,无论是进程、线程,到了内核我们统一称之为任务( Task ),由一个叫做task_struct

的结构统一管理,这个结构体包含了一个进程所需要的所有信息。接下来我们基于 kernel 5.2来分析这个结构。

0x00 task_struct 分析

首先 task_struct 结构体位于 /linux5.2/include/linux/sched.h 文件中。

0x01 任务状态

每一个任务都需要有一个状态,涉及到任务状态的是下面的几个变量:

	/* -1 unrunnable, 0 runnable, >0 stopped: */
	volatile long			state;
	/* Per task flags (PF_*), defined further below: */
	unsigned int			flags;
	int				exit_state;

state状态的值可以取以下:

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

/* get_task_state(): */
#define TASK_REPORT			(TASK_RUNNING | TASK_INTERRUPTIBLE | \
					 TASK_UNINTERRUPTIBLE | __TASK_STOPPED | \
					 __TASK_TRACED | EXIT_DEAD | EXIT_ZOMBIE | \
					 TASK_PARKED)

每个进程都必然处于以上所有状态中的一种

  1. TASK_RUNNING 表示进程时刻准备着运行的状态。当处于这个状态的进程获得时间片的时候,就是运行中;如果没有获得时间片,那么说明被其他进程强占了,再等待再次分配时间片。
  2. TASK_INTERRUPTIBLE,可中断的睡眠状态。当前进程阻塞状态,虽然此时在睡眠状态,但可以被信号唤醒,唤醒后进行信号的处理,此时处理的函数可以由程序员来决定。
  3. TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。此时在睡眠状态时,不可被信号唤醒,只能等待I/O操作完成。如果在I/O过程中发生意外无法完成,那么就会形成一个无法唤醒的进程。包括 kill 命令也无法唤醒,因为 kill 本身就是一种信号。
  4. TASK_KILLABLE,可终止的睡眠状态。从上面的注释可以发现为了唤醒而使用的宏,它设置的值是TASK_UNINTERRUPTIBLE 和 TASK_WAKEKILL ,即在 TASK_UNINTERRUPTIBLE 的基础上 可以响应致命唤醒信号。
  5. __TASK_STOPPED ,被停止状态。 在进程接收到 SIGTTIN、SIGSTOP、SIGTSTP、STGTTOU信号后会进入此状态。
  6. __TASK_TRACED 进程被 debugger 等进程监视。
  7. EXIT_ZOMBIE 进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息。
  8. EXIT_DEAD 进程的最终状态。
0x02 任务ID

每个任务都需要有一个标识符,这个标识可以用来做下发指令和任务显示。

	pid_t				pid;
	pid_t				tgid;
	struct task_struct		*group_leader;
  • pid 是 process id
  • tgid是 thread group id
  • group_leader 指针用于快速访问

如果一个进程只有主线程,那么pid和tgid都是自己,group_leader指向的也是自己。

如果进程创建了其他线程,线程有自己的pid,tgid就是主线程的pid,group_leader指向的是进程的主线程。

那么一个进程中 pid 的取值范围是多少呢

查看 /include/linux/threads.h 文件会有这样一个宏

#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

在CONFIG_BASE_SMALL为0的情况下,pid的取值范围是0到32767。

0x03 内核是如何把任务串联起来的
	struct list_head		tasks;

我们发现有一个 list_head的结构体,这是一个链表。没错,内核就是通过链表将任务串联起来的。

那么为什么使用链表 而不是数组。数组是连续的内存空间更适合存储啊。

这个是因为要维护众多的 task 关系,一个task 节点的parent 指针指向其父进程的 task,children指针指向子进程所有task的头部,然后又靠sibling指针来维护兄弟 task。所以很明显链表结构更合适。

0x04 进程亲属关系

在 linux 中,任何一个进程都拥有一个父进程,所有进程之间都有着直接或者间接的联系,拥有同一父进程的所有进程称之为兄弟进程,其实整个进程就是一颗进程树。

  struct task_struct __rcu	*real_parent;
	struct task_struct __rcu	*parent;
	struct list_head		children;
	struct list_head		sibling;
	struct task_struct		*group_leader;
  • real_parent 指向其父进程,如果创建它的父进程不存在,则指向PID为1的init进程。
  • parent 指向其父进程, 当它终止时必须向它的父进程发信号。值通常与 real_parent 相同。
  • children 表示链表的头部,链表中的所有元素都是它的子进程。
  • sibling 用于把当前进程插入到兄弟链表中。
  • group_leader 指向其所在进程组的 learder 进程。
0x05 进程权限

一个任务能访问哪些文件,能访问哪些其他的任务,以及可以被哪些任务所访问,这些问题都是由权限系统来控制。

那么 linux 进程中的权限到底是怎么样的存在?

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

根据注释就能知道 real_cred是被操作的权限,**cred **是这个进程操作其他的权限。

__rcu(Read-Copy update) 共享数据的同步机制

接下来我们看一个 cred 这个结构体,位于 cred.h 中

	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
  • uid、gid 表示这个进程的启动者,谁启动的就是谁的id。
  • euid、egid 当一个进程要访问资源时,比较的就是这个用户和组是否有权限。
  • fsuid、fsgid 这个是对文件操作所需要的比较的权限。

一般来说,fsuid、euid和uid 是一样的, fsuid、egid和gid也是一样的。

linux 通过SUID(Set User ID on execution) 来赋予权限的话,会带来安全隐患,那么为了解决这个问题,linux 引用了capabilities 能力机制

capabilities 机制将 root 用户的权限细分,可以分别的启动和禁用,在实际的操作当中,如果euid 不是root,便会检查该特权具有哪些capabilities。

在linux 操作系统中 可以查看/proc/{PID}/status查看进程所属属性

接来下 查看一下 capabilities定义了哪些常见的权限

#define CAP_CHOWN            0
#define CAP_KILL             5
#define CAP_SETUID           7
#define CAP_NET_BIND_SERVICE 10
#define CAP_NET_RAW          13
#define CAP_SYS_MODULE       16
#define CAP_SYS_RAWIO        17
#define CAP_SYS_PTRACE       19
#define CAP_SYS_BOOT         22
#define CAP_SYS_TIME         25
#define CAP_AUDIT_READ          37
#define CAP_LAST_CAP         CAP_AUDIT_READ
  • cap_permitted 表示进程能够使用的权限
  • cap_effective 实际起作用的权限
  • cap_inheritable 表示当可执行文件的设置了 inheritable 位时,调用exec执行该程序会继承调用者的inheritable 集合,并将其加入到permitted集合中。在非root用户下执行exec时 通常不会保存
  • cap_ambient 非root用户使用 exec 执行一个程序时,cap_ambient会被加入到 cap_permitted和cap_effective中。
  • cap_bset 进程中所有进程允许保留的集合。
0x06 内核态函数栈

在 task_struct 结构中,内核栈是由 stack 变量管理

	void				*stack;

内核栈位于pt_regs和thread_info之间:pt_regs 、内核栈、thread_info

内核栈是从该内存内存空间的自顶向下(从高位到地位),而thread_info 是从底向上的(从地位到高位)。内核栈的栈顶地址存储在esp中。所以当用户从用户态进入内核态后 esp会指向内核栈的底部。

通过 thread_info 获取task_struct:

在include/linux/thread_info.h文件中 current_thread_info() 方法

#define current_thread_info() ((struct thread_info *)current)

在arch/x86/include/asm/current.h

static __always_inline struct task_struct *get_current(void)
{
	return this_cpu_read_stable(current_task);
}

内核栈的大小为:

  • 在32位系统上 arch/x86/include/asm/page_32_types.h
#define THREAD_SIZE_ORDER	1
#define THREAD_SIZE		(PAGE_SIZE << THREAD_SIZE_ORDER)
  • 在64位系统上 arch/x86/include/asm/page_64_types.h
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER	(2 + KASAN_STACK_ORDER)
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

那么PAGE_SIZE是多少呢?来看一下page.h,出现了一个_AC和一个UL的宏

/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT	12
#define PAGE_SIZE	(_AC(1, UL) << PAGE_SHIFT)

接下来看一下const.h中UL的定义

#define UL(x)		(_UL(x))
#define ULL(x)		(_ULL(x))

再看一下const.h中对_AC和_UL的定义。也就是说 在非汇编情况下__AC 贴合了XY2个宏

#ifdef __ASSEMBLY__
#define _AC(X,Y)	X
#define _AT(T,X)	X
#else
#define __AC(X,Y)	(X##Y)
#define _AC(X,Y)	__AC(X,Y)
#define _AT(T,X)	((T)(X))
#endif

#define _UL(x)		(_AC(x, UL))
#define _ULL(x)		(_AC(x, ULL))

#define _BITUL(x)	(_UL(1) << (x))
#define _BITULL(x)	(_ULL(1) << (x))

从上面3段代码分析可以得出:

  • 对于32位系统 _AC(1,UL) 是 1 ,左移12位后PAGE_SIZE是4096。左移1位后THREAD_SIZE8192
  • 对于64位系统,在非KASAN情况下,将PAGE_SIZE左移2位后THREAD_SIZE16384

我们知道了内核栈的大小之后,就会考虑当系统调用从用户态到内核态的时候,用户状态是如何保存的,此时需要用到一个pt_regs的结构,将用户态运行过程中CPU的上下文信息保存在寄存器中,这样当内核态系统调用返回的时候,进程就会接着运行下去了。

内存栈的最高地址端,存放的是一个 pt_regs 结构 /arch/x86/include/uapi/asm/ptrace.h

//32位
struct pt_regs {
	long ebx;
	long ecx;
	long edx;
	long esi;
	long edi;
	long ebp;
	long eax;
	int  xds;
	int  xes;
	int  xfs;
	int  xgs;
	long orig_eax;
	long eip;
	int  xcs;
	long eflags;
	long esp;
	int  xss;
};
//64位
struct pt_regs {
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long rbp;
	unsigned long rbx;
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long rax;
	unsigned long rcx;
	unsigned long rdx;
	unsigned long rsi;
	unsigned long rdi;
	unsigned long orig_rax;
	unsigned long rip;
	unsigned long cs;
	unsigned long eflags;
	unsigned long rsp;
	unsigned long ss;
};

你可能感兴趣的:(linux)