Linux(内核剖析):04---进程之struct task_struct进程描述符任务结构介绍

少年乘勇气,百战过乌孙。这篇文章主要讲述Linux(内核剖析):04---进程之struct task_struct进程描述符任务结构介绍相关的知识,希望能为你提供帮助。
一、进程描述符(struct task_struct)、任务结构

任务队列
  • 内核把进程的列表存放在叫做任务队列(task list) 的双向循环链表中。链表中的每一 项都是类型为task_struct
  • 备注:有些操作系统会把任务队列称为任务数组。但是Linux实现时使用的是队列而不是静态数组,所以称为任务队列
【Linux(内核剖析):04---进程之struct task_struct进程描述符任务结构介绍】
Linux(内核剖析):04---进程之struct task_struct进程描述符任务结构介绍

文章图片

进程描述符(struct task_struct)
  • task_struct称为进程描述符(process descriptor) 结构,该结构定义在< linux/sched.h> 文件中。进程描述符中包含一个具体进程的所有信息
  • 进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态等
  • task_struct相对较大,在32位机器上,它大约有1.7KB。
  • 以下代码来自Linux-2.6.22/include/linux/sched.h
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, > 0 stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int lock_depth; /* BKL lock depth */#ifdef CONFIG_SMP #ifdef __ARCH_WANT_UNLOCKED_CTXSW int oncpu; #endif #endif int load_weight; /* for niceness load balancing purposes */ int prio, static_prio, normal_prio; struct list_head run_list; struct prio_array *array; unsigned short ioprio; #ifdef CONFIG_BLK_DEV_IO_TRACE unsigned int btrace_seq; #endif unsigned long sleep_avg; unsigned long long timestamp, last_ran; unsigned long long sched_time; /* sched_clock time spent running */ enum sleep_type sleep_type; unsigned int policy; cpumask_t cpus_allowed; unsigned int time_slice, first_time_slice; #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT) struct sched_info sched_info; #endif struct list_head tasks; /* * ptrace_list/ptrace_children forms the list of my children * that were stolen by a ptracer. */ struct list_head ptrace_children; struct list_head ptrace_list; struct mm_struct *mm, *active_mm; /* task state */ struct linux_binfmt *binfmt; int exit_state; int exit_code, exit_signal; int pdeath_signal; /*The signal sent when the parent dies*/ /* ??? */ unsigned int personality; unsigned did_exec:1; pid_t pid; pid_t tgid; #ifdef CONFIG_CC_STACKPROTECTOR /* Canary value for the -fstack-protector gcc feature */ unsigned long stack_canary; #endif /* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively.(p-> father can be replaced with * p-> parent-> pid) */ struct task_struct *real_parent; /* real parent process (when being debugged) */ struct task_struct *parent; /* parent process */ /* * children/sibling forms the list of my children plus the * tasks I\'m ptracing. */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent\'s children list */ struct task_struct *group_leader; /* threadgroup leader */ /* PID/PID hash table linkage. */ struct pid_link pids[PIDTYPE_MAX]; struct list_head thread_group; struct completion *vfork_done; /* for vfork() */ int __user *set_child_tid; /* CLONE_CHILD_SETTID */ int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */ unsigned int rt_priority; cputime_t utime, stime; unsigned long nvcsw, nivcsw; /* context switch counts */ struct timespec start_time; /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ unsigned long min_flt, maj_flt; cputime_t it_prof_expires, it_virt_expires; unsigned long long it_sched_expires; struct list_head cpu_timers[3]; /* process credentials */ uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; struct group_info *group_info; kernel_cap_tcap_effective, cap_inheritable, cap_permitted; unsigned keep_capabilities:1; struct user_struct *user; #ifdef CONFIG_KEYS struct key *request_key_auth; /* assumed request_key authority */ struct key *thread_keyring; /* keyring private to this thread */ unsigned char jit_keyring; /* default keyring to attach requested keys to */ #endif /* * fpu_counter contains the number of consecutive context switches * that the FPU is used. If this is over a threshold, the lazy fpu * saving becomes unlazy to save the trap. This is an unsigned char * so that after 256 times the counter wraps and the behavior turns * lazy again; this to deal with bursty apps that only use FPU for * a short time */ unsigned char fpu_counter; int oomkilladj; /* OOM kill score adjustment (bit shift). */ char comm[TASK_COMM_LEN]; /* executable name excluding path - access with [gs]et_task_comm (which lock it with task_lock()) - initialized normally by flush_old_exec */ /* file system info */ int link_count, total_link_count; #ifdef CONFIG_SYSVIPC /* ipc stuff */ struct sysv_sem sysvsem; #endif /* CPU-specific state of this task */ struct thread_struct thread; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; /* namespaces */ struct nsproxy *nsproxy; /* signal handlers */ struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked, real_blocked; sigset_t saved_sigmask; /* To be restored with TIF_RESTORE_SIGMASK */ struct sigpending pending; unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; void *security; struct audit_context *audit_context; seccomp_t seccomp; /* Thread group tracking */ u32 parent_exec_id; u32 self_exec_id; /* Protection of (de-)allocation: mm, files, fs, tty, keyrings */ spinlock_t alloc_lock; /* Protection of the PI data structures: */ spinlock_t pi_lock; #ifdef CONFIG_RT_MUTEXES /* PI waiters blocked on a rt_mutex held by this task */ struct plist_head pi_waiters; /* Deadlock detection and priority inheritance handling */ struct rt_mutex_waiter *pi_blocked_on; #endif#ifdef CONFIG_DEBUG_MUTEXES /* mutex deadlock detection */ struct mutex_waiter *blocked_on; #endif #ifdef CONFIG_TRACE_IRQFLAGS unsigned int irq_events; int hardirqs_enabled; unsigned long hardirq_enable_ip; unsigned int hardirq_enable_event; unsigned long hardirq_disable_ip; unsigned int hardirq_disable_event; int softirqs_enabled; unsigned long softirq_disable_ip; unsigned int softirq_disable_event; unsigned long softirq_enable_ip; unsigned int softirq_enable_event; int hardirq_context; int softirq_context; #endif #ifdef CONFIG_LOCKDEP # define MAX_LOCK_DEPTH 30UL u64 curr_chain_key; int lockdep_depth; struct held_lock held_locks[MAX_LOCK_DEPTH]; unsigned int lockdep_recursion; #endif/* journalling filesystem info */ void *journal_info; /* stacked block device info */ struct bio *bio_list, **bio_tail; /* VM state */ struct reclaim_state *reclaim_state; struct backing_dev_info *backing_dev_info; struct io_context *io_context; unsigned long ptrace_message; siginfo_t *last_siginfo; /* For ptrace use.*/ /* * current io wait handle: wait queue entry to use for io waits * If this thread is processing aio, this points at the waitqueue * inside the currently handled kiocb. It may be NULL (i.e. default * to a stack based synchronous wait) if its doing sync IO. */ wait_queue_t *io_wait; #ifdef CONFIG_TASK_XACCT /* i/o counters(bytes read/written, #syscalls */ u64 rchar, wchar, syscr, syscw; #endif struct task_io_accounting ioac; #if defined(CONFIG_TASK_XACCT) u64 acct_rss_mem1; /* accumulated rss usage */ u64 acct_vm_mem1; /* accumulated virtual memory usage */ cputime_t acct_stimexpd; /* stime since last update */ #endif #ifdef CONFIG_NUMA struct mempolicy *mempolicy; short il_next; #endif #ifdef CONFIG_CPUSETS struct cpuset *cpuset; nodemask_t mems_allowed; int cpuset_mems_generation; int cpuset_mem_spread_rotor; #endif struct robust_list_head __user *robust_list; #ifdef CONFIG_COMPAT struct compat_robust_list_head __user *compat_robust_list; #endif struct list_head pi_state_list; struct futex_pi_state *pi_state_cache; atomic_t fs_excl; /* holding fs exclusive resources */ struct rcu_head rcu; /* * cache last used pipe for splice */ struct pipe_inode_info *splice_pipe; #ifdef CONFIG_TASK_DELAY_ACCT struct task_delay_info *delays; #endif #ifdef CONFIG_FAULT_INJECTION int make_it_fail; #endif };

二、分配进程描述符(struct thread_info)
  • Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring) 的目的(通过预先分配和重复使用task_sturct,可以避免动态分配和释放所带来的资源消耗)
  • 进程描述符的分配:
    • 在2.6以前的内核中,各个进程的task struct存放在它们栈的尾端。这样做是为了让那些像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专记录
    • 由于现在用slab分配器动态生成task_ struct,所以只需在栈底(对于向下增长的栈来说)或栈顶 (对于向上增长的栈来说)创建一个新的结构struct thread_info(寄存器较弱的体系结构不是引入thread_info结构的唯一原因。这个新建的结构使在汇编代码中计算其偏移变得非常容易)
struct thread_info
  • 每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针
  • 以下代码来自Linux-2.6.22/include/asm-i386/thread_info.h
struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ unsigned longflags; /* low level flags */ unsigned longstatus; /* thread-synchronous flags */ __u32cpu; /* current CPU */ intpreempt_count; /* 0 => preemptable, < 0 => BUG */ mm_segment_taddr_limit; /* thread address space: 0-0xBFFFFFFF for user-thead 0-0xFFFFFFFF for kernel-thread*/ void*sysenter_return; struct restart_blockrestart_block; unsigned longprevious_esp; /* ESP of the previous stack in case of nested (IRQ) stacks*/ __u8supervisor_stack[0]; };

Linux(内核剖析):04---进程之struct task_struct进程描述符任务结构介绍

文章图片

三、进程描述符的存放(pid、current_thread_info宏)
  • 内核通过一个唯一的进程标识 (process identification value)或PID来标识每个进程
PID的取值范围
  • PID是 —个数,表示为pid _t隐含类型,实际上就是一个int类型
  • 为了与老版本的Unix和Linux兼容, PID的最大值默认设置为32768 (short int短整型的最大值),尽管这个值也可以增加到高达400万(这受< linux/threads.h> 中所定义PID最大值的限制)。内核把每个进程的PID存放在它们各自的进程描述符中
  • 以下代码来自Linux-2.6.22/include/linux/threads.h
/* * This controls the default maximum pid allocated to a process */ #define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)/* * A maximum of 4 million PIDs should be enough for a while. * [NOTE: PID/TIDs are limited to 2^29 ~= 500+ million, see futex.h.] */ #define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \\ (sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))

PID取值范围的修改
  • 这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管32768对于一般的桌面系统足够用了,但是大型服务器可能需要更多进程。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样一来就破坏了这一原则。如果确实需要的话,可以不考虑与老式系统的兼容,由系统管理员通过修改/proc/sys/kernel/pid_max来提高上限
current宏
  • 在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要
  • 不同体系current宏的实现:
    • 硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理
    • 有的硬件体系结构可以拿出一个专门寄存器来存放指向当进程task_struct的指针,用于加快访问速度
    • 而有些像x86这样的体系结 (其寄存器并不富余),就只能在内栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构
  • x86系统上,current栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移。 该操作是通过current_thread_info()函数来完成的。汇编代码如下:
//备注:这里假定栈的大小为8KB 。当4KB的栈启用时,就要用4096,而不是8192movl $-8192, %eax andl %esp, %eax

  • 最后,current再从thread_info的task域中提取并返回task_struct的地址:
current_thread_info()-> task;

  • 对比一下这部分在PowerPC上的实现(IBM基于RISC的现代微处理器),我们可以发现PPC当前的task_struct是保存在一个寄存器中的。也就是说,在PPC上,current宏只需把r2寄存器中的值返回就行了。与x86不一样,PPC有足够多的寄存器,所以它的实现有这样选择的余地。而访问进程描述符是一个重要的频繁操作,所以PPC内核开发者觉得完全有必要为此使用一个专门的寄存器
四、进程状态
  • 进程描述符中的state域描述了进程的当前状态
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, > 0 stopped */ /*...*/ };

  • 系统中的每个进程都必然处于五种进程状态中的一种。该域的值也必为下列五种状态标志之一:
    • TASK_RUNNING(运行)——进程是可执行的;它或者正在执行,或者在运行队列中等待执行(运行队列将会在后面文章讨论)。这是进程在用户空间中执行的唯一可能的状态; 这种状态也可以应用到内核空间中正在执行的进程
    • TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
    • TASK_UNINTERRUPTIBLE (不可中断)——除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用得较少
      • 这就是你在执ps命令时,看到那些被标为D状态而又不能被杀死的进程的原因。由于任务将不响应信号,因此,你不可能给它发送SIGKILL信号。退一步说,即使有办法,终结这样一个任务也不是明智的选择 ,因为该任务有可能正在执行重要的操作,甚至还可能持有一个佶号置
    • __TASK_TRACED——被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪
    • __TASK_TSTOPPED(停止)——进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTO U等信号的时候。此外,调试期间接收到任何信号,都会使进程进入这种状态
Linux(内核剖析):04---进程之struct task_struct进程描述符任务结构介绍

文章图片

五、设置当前进程状态(set_task_state)
  • 内核经常需要调整某个进程的状态。这时最好使用set_task_state(task,state)函数
set_task_state(task, state); /* 将任务task的状态设置为state */

  • 该函数将指定的进程设置为指定的状态。必要的时候,它会设置内存屏障来强制其他处理器作重新排序。(一般只有在SMP系统中有此必要)否则,它等价于:
task-> state = state;

  • set_current_state(state)和set_task_state(current,state)含义是等同的。可以参考< linux/sched.h> 中对这些相关函数实现的说明
六、进程上下文(用户空间、内核空间)
  • 可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行
  • 一般程序在用户空间执行。当一个程序调执行了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间会继续执行
七、进程家族树
  • Unix系统的进程之间存在一个明显的继承关系,在Linux系统中也是如此
    • 所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个过程
    • 系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟
    • 进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程tast_struct、叫做parent的指针,还包含一个称为children的子进程链表
  • 所以,对于当前进程,可以通过下面的代码获得其父进程的进程描述符:
struct task_struct *my_parent = current-> parent;

  • 同样,也可以按以下方式依次访问子进程:
struct task_struct *task; struct list_head *list; list_for_each(list,& current-> childrn){ //task指向当前的某个子进程 task=list_entry(list,struct task_struct,sibling); }

  • init进程的进程描述符是作为init_task静态分配的。下面的代码可以很好地演示所有进程之间的关系:
struct task_struct *task; for(task=current; task!=& init_task; task=task-> parent) /*...*//*for循环之后,task指向于init*/

  • 实际上,你可以通过这种继承体系从系统的任何一个进程出发查找到任意指定的其他进程。 但大多数时候,只需要通过简单的重复方式就可以遍历系统中的所有进程。这非常容易做到,因为任务队列本来就是一个双向的循环链表。对于给定的进程,获取链表中的下一个进程:
list_entry(task-> tasks.next,struct task_struct, tasks);

  • 获取前一个进程的方法与之相同:
list_entry(task-> tasks.prev,struct task_struct,tasks);

  • 这两个例程分别通过next_task(task) 宏和prev_task(task)宏实现。而实际上,for_each_process(task)宏提供了依次访问整个任务队列的能力。每次访问,任务指针都指向链表中的下一个元素:
struct task_struct *task; for_each__process (task) { /*它打印每一个任务的名称和PID*/ printk("%s[%d]\\n", task-> comm,task-> pid); }

  • 备注:在一个拥有大量进程的系统中通过重复来遍历所有的进程代价是很大的。因此,如果没有充足的理由(或者别无他法),别这样做

    推荐阅读