task_struct 结构描述
在 Linux 中每个进程都是由一个 task_struct 结构来进行描述的。通常我们常说的 PBC (进程控制块)就是指 task_struct。
task-struct 结构包含了进程的所有信息,它是系统对进程进行控制的有效手段。
task_struct 结构进行具体描述如下
struct task_struct {
volatile long state; unsigned long flags; int sigpending; mm_segment_t addr_limit; struct exec_domain *exec_domain; volatile long need_resched; unsigned long ptrace; //上下文切换时内核锁的深度 int lock_depth; long counter; //进程剩余的时间片 long nice; unsigned long policy; //描述进程的地址空间 struct mm_struct *mm; int processor; //进程当前正在使用的CPU struct list_head run_list; //运行队列的链表 unsigned long sleep_time; //进程在双向循环链表中的链接 struct task_struct *next_task, *prev_task; struct mm_struct *active_mm; //内核线程所借用的地址空间 struct list_head local_pages; unsigned int allocation_order, nr_local_pages;
//指向进程所属的全局执行文件格式结构,共有a.out、script、elf、java 等4 种
struct linux_binfmt binfmt;
//程序的返回代码以及程序异常终止产生的信号,这些数据由父进程(子进程完成后)轮流查询
int exit_code, exit_signal;
int pdeath_signal; / The signal sent when the parent dies /
/ ??? */
unsigned long personality;
//按POSIX 要求设计的布尔量,区分进程正在执行老程序代码,还是用系统调用execve()装入一个新的程序
int did_exec:1;
pid_t pid; //进程标识符 int leader; struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct list_head thread_group; //进程在哈希表中的链接 struct task_struct *pidhash_next; struct task_struct **pidhash_pprev; wait_queue_head_t wait_chldexit; struct completion *vfork_done; unsigned long rt_priority; //实时优先级 unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; struct tms times; unsigned long start_time; //进程创建时间 long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
//每一个进程可以通过系统调用setlimit 和getlimit 来限制它资源的使用
struct rlimit rlim[RLIM_NLIMITS];
unsigned short used_math;
//这个域存储进程执行的程序的名字,这个名字用在调试中
char comm[16];
struct sem_undo *semundo; //为避免死锁而在信号量上设置的取消操作 struct sem_queue *semsleeping; //与信号量操作相关的等待队列
struct fs_struct fs; //进程的可执行映像所在的文件系统
/ open file information */
struct files_struct files; //进程打开的文件
/ signal handlers /
//信号掩码的自旋锁
spinlock_t sigmask_lock; / Protects signal and blocked */
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //信号掩码 ...
};
进程就是处于执行期的程序,但进程不仅仅局限于一段可执行程序代码(也就是所谓的代码段,text section),从上面的数据结构可以看到,进程还包含其他的资源,比如打开的文件,挂起的信号,处理器状态,内核数据结构,内存映射地址空间等。
在操作系统中,内核的调度对象时线程,而不是进程。线程时进程中的活动对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程程寄存器。
在传统的 Unix 系统中,一个进程只包含一个线程,但是在现代操作系统中,一个进程可以包含多个线程。在 Linux 系统中线程的实现非常特别:它对线程和进程并不特别区分。对于 Linux 而言,线程只不过是一种特殊的进程罢了。后续的文章专门介绍进程和线程的创建过程进行分析。
在操作系统中,内核把进程的列表存放在一个叫任务队列的双向循环链表中,链表中的每个元素类型就是上述的数据结构 task_struct, 称为进程描述符的结构。该结构中包含了具体进程的所有信息。task_struct 在32位机器上,大约有1.7KB的大小。
task_struct 结构在内存中的存放
在分析之前,需要了解下一个概念 – 内核栈。
我们知道一个在32系统中,进程的虚拟地址空间大小为4G。在这4G虚拟机制空间中有一段虚拟地址空间为栈的区域,该栈的区域为用户态栈。该栈记录的是在用户态进程的函数调用过程。
图片
但是当进程进行系统调用时进入内核态时,在内核中使用的栈不再是上述的用户态的栈了,而是单独的内核空间的栈,称为内核栈。每个进程都有各自的内核栈。该栈是在进程创建时生成的。
当进程从用户态进入内核态时,CPU 就自动地设置该进程的内核栈,也就是说,CPU 从任务状态段 TSS 中装入内核栈指针 esp。
在 Intel 系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的,后续有数据开始写入栈中时,esp 的值就递减。
内核栈在不同的版本中表示的也不大相同,在2.4版本中内核栈空间和 task_struct 结构时存放到一块的。
//2.4内核栈表示方式
union task_union {
struct task_struct task;
unsigned long stack[INIT_TASK_SIZE/sizeof(long)];
};
从这个结构可以看出,内核栈占8KB 的内存区。实际上,进程的task_struct 结构所占的内存是由内核动态分配的,更确切地说,内核根本不给 task_struct 分配内存,而仅仅给内核栈分配8KB 的内存,并把其中的一部分给 task_struct 使用。
task_struct 结构大约占1K 字节左右,其具体数字与内核版本有关,因为不同的版本其域稍有不同。因此,内核栈的大小不能超过7KB,否则,内核栈会覆盖 task_struct 结构,从而导致内核崩溃。不过,7KB 大小对内核栈已足够。
把 task_struct 结构与内核栈放在一起具有以下好处:
内核可以方便而快速地找到这个结构,用伪代码描述如下:
task_struct = (struct task_struct *) STACK_POINTER & 0xffffe000
避免在创建进程时动态分配额外的内存。
task_struct 结构的起始地址总是开始于页大小(PAGE_SIZE)的边界。
内核栈的分布图如下:
图片
获取当前进程指针时,操作如下:
static inline struct task_struct * get_current(void)
{
struct task_struct *current;
asm(“andl %%esp,%0; “:”=r” (current) : “0” (~8191UL));
return current;
}
#define current get_current()
实际上,这段代码相当于如下一组汇编指令(设p 是指向当前进程task_struc 结构的指针):
movl $0xffffe000, %ecx
andl %esp, %ecx
movl %ecx, p
换句话说,仅仅只需检查栈指针的值,而根本无需存取内存,内核就可以导出
task_struct 结构的地址。
在2.6以前,各个进程的 task_struct 存放在他们内核栈的尾端。这样做的目的是为了让那些像X86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。
而在2.6中使用的 slab 分配器动态的生成 task_struct 结构,所以只需在栈底(对于向下增长的栈而言)或栈顶(对于向上增长的栈而言)创建一个新的数据结构 thread_info。这个新建的数据结构在汇编代码中计算器偏移量变得非常容易。
其结构如下:
//2.6 内核栈表示方式
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
thread_info 结构如下:
struct thread_info {
struct task_struct task; / main task structure /
__u32 cpu; / current CPU */
...
};
在2.6中的 task_struct 中,存在一个stack 字段指向 thread_info
struct task_struct {
volatile long state;
void *stack; // 指向 thread_info 的指针
…
}
内核栈的分布图如下:
图片
当获取当前进程信息时,可通过 current_thread_info() 获取。
#define get_current() (current_thread_info()->task)
#define current get_current()
在X86系统上,current 都是把栈指针的后13个有效位屏蔽掉,用来计算偏移进行获取的。,汇编代码如下:
movl $-8192, %eax
andl %esp, %eax
最后,current 再从 thread_info 的 task 域中提取并返回 task_struct 的地址
current_thread_info()->task
进程状态
在 task_struct 中 state 描述了进程的当前状态。系统中的每个进程一定处于如下5中状态中的一种,因此 state 也必为如下5种状态标志之一。
TASK_RUNNING(可运行): 进程是可执行的,或者正在执行,或者在运行队列中等待执行。正在运行的进程就是当前进程(由 current 所指向的进程),而准备运行的进程只要得到CPU 就可以立即投入运行,CPU 是这些进程唯一等待的系统资源。系统中有一个运行队列,用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行
TASK_INTERRUPTIBLE(可中断): 进程正在睡眠(阻塞),在等待某些条件的达成,一旦这些条件达成,内核就把进程状态设置为可运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
TASK_UNINTERRUPTIBLE(不可中断):除了就算是接收到信号也不会被唤醒或者准备投入运行外,这个状态与可打断状态相同。该状态通常在进程必须在等待时不受干扰或者等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用的较少。
TASK_ZOMBIE(僵死):进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
TASK_STOPPED(暂停):进程停止执行,进程没有投入运行也不能投入运行。通常这种状态发生在接收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 等信号的时候,此外,在调试期间接收到的任何信号,都会是进程进入这种状态。
状态之间的切换关系如图:
图片
原文链接
Linux 进程管理之基础知识
本文只介绍了进程的内核基础知识,后续通过源码进一步分析相关进程管理的知识。
公众号 Linux码农 推荐阅读
Linux ‘网络配置’ 和 ‘故障排除’ 命令总结
Linux 进程管理之基础知识
你需要了解的55个网络概念
Centos7 开启 iptables 日志
memcache 多线程模型
linux ulimit 调优
Linux GDB的实现原理
服务端 TCP 连接的 TIME_WAIT 过多问题的分析与解决
60秒内对 Linux 进行性能诊断
Linux 下的资源限制
你需要了解的55个网络概念
常见的限流方式之漏桶算法
常见的限流方式之计数器滑动窗口算法
应该知道的LINUX技巧
Linux 可执行文件程序载入和执行过程
进程间通信(IPC) 系列 | mmap
一文讲懂什么是vlan、三层交换机、网关、DNS、子网掩码、MAC地址
关于 TCP/IP,必知必会的十个问题
高性能定时器策略之时间轮定时器算法
关注公众号 Linux码农 获取更多干货



