在 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)每个进程都必然处于以上所有状态中的一种
TASK_RUNNING 表示进程时刻准备着运行的状态。当处于这个状态的进程获得时间片的时候,就是运行中;如果没有获得时间片,那么说明被其他进程强占了,再等待再次分配时间片。TASK_INTERRUPTIBLE,可中断的睡眠状态。当前进程阻塞状态,虽然此时在睡眠状态,但可以被信号唤醒,唤醒后进行信号的处理,此时处理的函数可以由程序员来决定。TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。此时在睡眠状态时,不可被信号唤醒,只能等待I/O操作完成。如果在I/O过程中发生意外无法完成,那么就会形成一个无法唤醒的进程。包括 kill 命令也无法唤醒,因为 kill 本身就是一种信号。TASK_KILLABLE,可终止的睡眠状态。从上面的注释可以发现为了唤醒而使用的宏,它设置的值是TASK_UNINTERRUPTIBLE 和 TASK_WAKEKILL ,即在 TASK_UNINTERRUPTIBLE 的基础上 可以响应致命唤醒信号。__TASK_STOPPED ,被停止状态。 在进程接收到 SIGTTIN、SIGSTOP、SIGTSTP、STGTTOU信号后会进入此状态。__TASK_TRACED 进程被 debugger 等进程监视。EXIT_ZOMBIE 进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息。EXIT_DEAD 进程的最终状态。每个任务都需要有一个标识符,这个标识可以用来做下发指令和任务显示。
pid_t pid; pid_t tgid; struct task_struct *group_leader; pid 是 process idtgid是 thread group idgroup_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。
我们发现有一个 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; real_parent 指向其父进程,如果创建它的父进程不存在,则指向PID为1的init进程。parent 指向其父进程, 当它终止时必须向它的父进程发信号。值通常与 real_parent 相同。children 表示链表的头部,链表中的所有元素都是它的子进程。sibling 用于把当前进程插入到兄弟链表中。group_leader 指向其所在进程组的 learder 进程。一个任务能访问哪些文件,能访问哪些其他的任务,以及可以被哪些任务所访问,这些问题都是由权限系统来控制。
那么 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 进程中所有进程允许保留的集合。在 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)内存栈的最高地址端,存放的是一个 pt_regs 结构 /arch/x86/include/uapi/asm/ptrace.h
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; }; 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; };