在 linux 里面,无论是进程、线程,到了内核我们统一称之为任务( Task ),由一个叫做task_struct
的结构统一管理,这个结构体包含了一个进程所需要的所有信息。接下来我们基于 kernel 5.2来分析这个结构。
首先 task_struct 结构体位于 /linux5.2/include/linux/sched.h 文件中。
每一个任务都需要有一个状态,涉及到任务状态的是下面的几个变量:
/* -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)
每个进程都必然处于以上所有状态中的一种
每个任务都需要有一个标识符,这个标识可以用来做下发指令和任务显示。
pid_t pid;
pid_t tgid;
struct task_struct *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。
struct list_head tasks;
我们发现有一个 list_head的结构体,这是一个链表。没错,内核就是通过链表将任务串联起来的。
那么为什么使用链表 而不是数组。数组是连续的内存空间更适合存储啊。
这个是因为要维护众多的 task 关系,一个task 节点的parent 指针指向其父进程的 task,children指针指向子进程所有task的头部,然后又靠sibling指针来维护兄弟 task。所以很明显链表结构更合适。
在 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;
一个任务能访问哪些文件,能访问哪些其他的任务,以及可以被哪些任务所访问,这些问题都是由权限系统来控制。
那么 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 */
一般来说,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
在 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);
}
内核栈的大小为:
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
#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段代码分析可以得出:
我们知道了内核栈的大小之后,就会考虑当系统调用从用户态到内核态的时候,用户状态是如何保存的,此时需要用到一个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;
};