分享

Linux进程管理之基础知识(每个进程都是由一个taskstruct结构来进行描述的(进程控制块)就是指taskstruct)

 山峰云绕 2023-02-21 发布于贵州


(每个进程都是由一个task struct结构来进行描述的(进程控制块)就是指taskstruct)

在 Linux 中每个进程都是由一个 task_struct 结构来进行描述的。通常我们常说的 PBC (进程控制块)就是指 task_struct

task-struct 结构包含了进程的所有信息,它是系统对进程进行控制的有效手段。

task_struct 结构进行具体描述如下

struct task_struct {

    /*
      进程执行时,它会根据具体情况改变状态,Linux 中的进程主要有如下状态
      TASK_RUNNING  可运行
      TASK_INTERRUPTIBLE  可中断的等待状态
      TASK_UNINTERRUPTIBLE 不可中断的等待状态
      TASK_ZOMBIE 僵死
      TASK_STOPPED  暂停

    */ 
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    unsigned long flags;    /* per process flags, defined below */
    int sigpending;
    mm_segment_t addr_limit;    /* thread address space:
                        0-0xBFFFFFFF for user-thead
                        0-0xFFFFFFFF for kernel-thread
                     */
    struct exec_domain *exec_domain;
    volatile long need_resched;
    unsigned long ptrace;
    //上下文切换时内核锁的深度
    int lock_depth;     /* Lock depth */

    long counter; //进程剩余的时间片
    long nice;
    /*进程调度策略,有3种

      SCHED_OTHER 其他调度, 普通进程调度策略
      SCHED_FIFO  先来先服务调度  实时进程调度策略
      SCHED_RR  时间片轮转调度   实时进程调度策略
    */ 
    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;
    /*
     active_mm,这是为内核线程而引入的。因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文
     切换方式,当内核线程进行上下文切换时,让切换进来的线程的active_mm 指向刚被调度出去的进程的
     active_mm(如果进程的mm 域不为空,则其active_mm 域与mm 域相同)。
    */
    struct mm_struct *active_mm; //内核线程所借用的地址空间
    struct list_head local_pages;
    unsigned int allocation_order, nr_local_pages;

/* task state */
    //指向进程所属的全局执行文件格式结构,共有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; //进程标识符

    /* boolean value for session group leader */
    int leader;
    /* 
     * pointers to (original) parent process, youngest child, younger sibling,
     * older sibling, respectively.  (p->father can be replaced with 
     * p->p_pptr->pid)
     */
    /*      p_opptr Original parent 祖先
      p_pptr Parent 父进程
      p_cptr Child 子进程
      p_ysptr Younger sibling 弟进程
      p_osptr Older sibling 兄进程
    */ 
    struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
    struct list_head thread_group;

    /* PID hash table linkage. */
    //进程在哈希表中的链接
    struct task_struct *pidhash_next;
    struct task_struct **pidhash_pprev;
    /*在进程结束时,或发出系统调用wait4 时,为了等待子进程的结束,而将自己(父进程)
      睡眠在该等待队列上,设置状态标志为TASK_INTERRUPTIBLE,并且把控制权转给调度程序*/
    wait_queue_head_t wait_chldexit;    /* for wait4() */
    struct completion *vfork_done;      /* for vfork() */
    unsigned long rt_priority; //实时优先级
    /*进程有3 种类型的定时器:
    实时定时器: 实时更新,即不论该进程是否运行 it_real_value、it_real_incr、real_timer
    虚拟定时器: 只在进程运行于用户态时更新 it_virt_value、it_virt_incr
    概况定时器: 进程运行于用户态和系统态时更新 it_prof_value、it_prof_incr
    */
    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; //进程创建时间
    /*
        per_cpu_utime 进程在某个CPU 上运行时在用户态下耗费的时间
        per_cpu_stime 进程在某个CPU 上运行时在系统态下耗费的时间
    */
    long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
     /*
     min_flat,maj_flt,nswap 进程累计的次(minor)缺页次数、主(major)次数及累计换出、换入页面数
     cmin_flat,cmaj_flt,cnswap 本进程作为祖先进程,其所有层次子进程的累计的次(minor)缺页次数、主(major)次数及累计换出、换入页面数  
     */
    unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
    int swappable:1; //进程占用的内存页面是否可换出
/* process credentials */
    /*
        Uid、gid  用户标识符、组标识符
        Euid、egid 有效用户标识符、有效组标识符
        Suid、sgid  备份用户标识符、备份组标识符
        Fsuid、fsgid 文件系统用户标识符、文件系统组标识符  
    */
    uid_t uid,euid,suid,fsuid;
    gid_t gid,egid,sgid,fsgid;
    ...
/* limits */
    //每一个进程可以通过系统调用setlimit 和getlimit 来限制它资源的使用
    struct rlimit rlim[RLIM_NLIMITS];
    unsigned short used_math;
    //这个域存储进程执行的程序的名字,这个名字用在调试中
    char comm[16];

    struct sem_undo *semundo; //为避免死锁而在信号量上设置的取消操作
    struct sem_queue *semsleeping; //与信号量操作相关的等待队列
/* CPU-specific state of this task */
    struct thread_struct thread;
/* filesystem information */
    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; /* -1 unrunnable, 0 runnable, >0 stopped */
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 等信号的时候,此外,在调试期间接收到的任何信号,都会是进程进入这种状态。

状态之间的切换关系如图:

本文只介绍了进程的内核基础知识,后续通过源码进一步分析相关进程管理的知识。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多