Linux操作系统 —— 进程(四)

Linux操作系统 —— 进程(四)

本系列,是自己学习Linux过程中的笔记。
希望读者在看完全文后,也能留下你们的经验或者问题。
如果能从这里学到点东西,记得请我喝杯☕☕☕~

—— MinRam

一、前言 Overview

进程,就是运行的程序。

进程 Process, 是操作系统提供给用户程序的一种抽象概念。内核篇讲过,程序本身是静态的一串代码,且存放在磁盘。在需要的时候由操作系统,将程序从磁盘中拿出,进行一系列操作后,程序开始运行,也就是进程。

而在操作系统中,往往会同时运行多个程序,浏览器、IDE、聊天工具等等,在现代操作系统中,往往会有几十个程序同时在跑。每个运行中的程序都需要跟CPU进行交互处理各式各样的计算任务。而CPU往往只有几个有限的核心。

面对这种僧多粥少的问题,操作系统给用户程序提供了CPU虚拟化技术,其目的就是产生无限多的CPU的假象,让每个用户程序觉得自己独占一个CPU核心。这一实现方式就是采用时分共享time sharing)实现的。这一技术的基础调度单位就是进程。

时分共享

可以看出,进程就是系统对CPU进行抽象,提供给应用程序使用。用户应用程序只跟进程打交道,无法触碰到实际CPU。而操作系统中进程管理子程序,也只关注进程这个抽象,再将它分配给实际CPU核心。这样用户程序并不需要关心当前有没有CPU可以用,而操作系统也不需要关心用户程序需不需要使用CPU。就这样操作系统实现了多个用户应用程序之间复用同一个或者多个CPU核心。

时分共享的机制,也运用在其他类型的资源上。

二、进程 Process

进程,程序的实例

2.1 进程的组成

对于操作系统来说,运行中的程序就是进程。我们可以通过了解进程运行过程中的一系列行为和操作,来了解进程。

那么需要了解对于执行进程来说,什么是需要我们关注的。所以进程由以下几个部分组成:

  • 进程标识 Process ID. 进程的标识符,除了本身的PID以外,还应当记录父进程的Parent PID
  • 进程状态 State 进程状态.
  • 虚拟内存地址空间 Address Space. 包括主存(用户空间、内核空间)、寄存器(一般寄存器、控制寄存器)、外部设备(文件、socket、其他内存映射IO设备)等. 程序的数据和变量都是存储在内存上,而程序本身的指令也是存在内存里的。也就是说整个进程都是位于内存上的。所以这一段内存的访问地址,就是进程的一部分。
  • 寄存器 Register. 特指特别功能范畴的寄存器,如程序计数器(Program Counter),也被叫做指令指针(Instruction Pointer),属于CPU寄存器,指向下一条指令所在的内存地址。还有栈指针(Stack Pointer) 和函数栈指针(Frame Pointer),则是用来指明函数入口参数,本地变量,以及范围值地址。
  • 外接设备信息 I/O infromation. 程序在运行中,可能会读取或者写入一些文件(如磁盘,U盘中的文件),并对其进行独占等操作,所以进程中还需有这些文件列表信息。

程序加载过程

2.2 进程的生命周期

对于操作系统,它提供了一些进程的操作接口API,也就对应了进程的生命周期:

  • 创建 Create. 进程可以通过多个途径被创建出来,如终端,双击快捷键。系统就会调用起一个进程。/
  • 销毁 Destroy. 有生就有死,操作系统必须有途径能强制性地销毁一条进程。正常情况下,在进程代码执行完后,往往会自行推出(exit)。但考虑到异常情况(如死循环,死锁)下,操作系统也能为用户提供销毁的接口(如linux的kill指令)
  • 等待 Wait. 有时候进程需要等待另一个进程的结束,也就是阻塞进程。
  • 其余操作 Miscellaneous Control. 除了销毁等待,操作系统还提供了其他的操作接口比如挂起(Suspend)和对应的恢复(Resume)
  • 状态 Status. 操作系统会提供一些接口,用来查询进程的运行状态,运行时常,PID等信息。

linux下对进程的API有以下几种(后续会详细介绍):
Linux Process API

2.3 进程的状态

随着操作系统发展,进程状态也在不断细分,但最基础的也是以下几种:

  • 创建状态 Creating. 前面讲到,在进程创建时,需要申请一块空白的进程控制块(PCB Process Controll Block)。并在向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态。进程的生命周期开始

  • 就绪状态 Ready. 在进程拿到所需资源后,就进入就绪态。这里包括创建准备完成,还有等待条件完成的进程。当进程处于就绪状态时,意味着进程随时可以被CPU激活进入执行状态。但由于执行策略的结果,没有被选中进入执行状态。

  • 执行状态 Running. 进程正在被执行,根据进程的指令执行相应的操作。

  • 阻塞状态 Blocked. 正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用。例如,当进程写入或者读取文件时,会等待I/O请求,这时候就会进入阻塞状态,而处理器就会去执行其他进程。

  • 终止状态 Dead. 进程结束,或出现错误,或被系统终止,进入终止状态。进程的生命周期结束

Process State

所以可以看出,进程的核心状态只有三种: 就绪Ready、执行Running、阻塞Blocked。但为用户观察需要,进程还有挂起和激活两种操作。挂起后进程处于静止状态进程不再被系统调用,相反操作则是激活操作。这里不再补充

三、 源码剖析 Code

Linux内核v5.13.16为例,且依赖库为X86(随便找的版本)。本节需要有一定的代码语法知识,特别是C语言。

操作系统,本身就是一个程序。同样有一些关键的数据结构,用来存储进程状态信息。其内容与上面谈到的逻辑大致一致,但也有具体的实现改动。以下结合代码,比较实际应用中的区别。

首先需要解释下Linux中,进程和线程的区别。在Linux操作系统层面,线程就是特殊的进程,在于跟其他同进程下的线程共享内存空间(代码段,数据段,但不共享栈)。在用户层面来说,进程和线程是两种不同的概念。而在内核层面中,线程其实也是进程。所以对于Linux内核来说,只有任务TASK这个概念。后续会单独出一章博客完整说明下Task定义。

回归开头,在Linux中,每个线程都是一个Task,也都是通过task_struct结构体来描述的,我们称它为进程描述符。内核进程相关源码链接(source/include/linux/sched.h。它包含了如下几个内容:

  • 标识符 : 唯⼀标⽰符,⽤来区别其他Task(线程)。
  • 状态Task当前状态。
  • 优先级 : 用于调度用的优先级。
  • 程序计数器Program Counter程序中即将被执⾏的下⼀条指令的地址。
  • 内存指针: 包括程序代码和线程相关数据的指针,还有和其他线程共享的内存块的指针
  • 上下文数据: 线程执⾏时处理器的寄存器中的数据。
  • I/O状态信息:包括显⽰的I/O请求,分配给线程的I/O设备和被线程使⽤的⽂件列表。
  • 运行信息: 包括处理器时间总和,使⽤的时钟数总和,时间限制等。

3.1 进程标识符 Process ID

进程标识符相关源码链接(source/include/linux/sched.h

  • 截取片段代码如下:
1
2
3
4
struct task_struct {
pid_t pid;
pid_t tgid;
}

前面讲到,对于Linux系统内核来说,只有线程和线程组。同一线程组内的线程共享内存空间(代码段,数据段,但不共享栈)。所以线程组就是进程的概念。

task_struct中,pid(Process ID)就是指线程的唯一标识,而tgid(Task Group ID)就是线程组的唯一标识。

  • 当创建一个新进程时,会先创建进程的主线程,主线程获取自己的PID,同时主线程的PID就是这个线程组(进程)的唯一标识TGID
  • 当一个线程启动一个新线程时,新线程会获取自己的PID,同时从原始线程继承TGID

进程与线程的区别

当我们用ps命令或者getpid()等接口查询进程ID时,内核返回给我们的也正是这个tgid。注意不是pid

pidtgid 都是 pid_t 类型,该类型是由各体系type.h分别定义的__kernel_pid_t,通常是一个 int 类型,参考include/uapi/asm-generic/posix_types.h

  • 截取部分代码如下:
    1
    2
    3
    4
    5
    6
    7
    // tags/v5.13.16 - include/linux/types.h
    typedef __kernel_pid_t pid_t

    // tags/v5.13.16 - include/uapi/asm-generic/posix_types.h
    #ifndef __kernel_pid_t
    typedef int __kernel_pid_t;
    #endif

threads.h可以看到关于PID默认情况下最大值的定义。

1
2
3
4
/*
* This controls the default maximum pid allocated to a process
*/
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

3.2 内核进程状态 Process Status

进程状态相关源码链接(source/include/linux/sched.h

  • 截取状态成员如下:
    1
    2
    3
    4
    5
    6
    struct task_struct {
    /* -1 unrunnable, 0 runnable, >0 stopped: */
    volatile long state;

    int exit_state;
    }

Task的主要状态由task->state确定,当进程退出后,会再task->exit_state同步更新。

  • 截取状态定义代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /*
    * Task state bitmask. NOTE! These bits are also
    * encoded in fs/proc/array.c: get_task_state().
    *
    * We have two separate sets of flags: task->state
    * is about runnability, while task->exit_state are
    * about the task exiting. Confusing, but this way
    * modifying one set can't modify the other one by
    * mistake.
    */

    /* 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)

可以看到基础进程状态的标识码都是采用long类型的,且都是2的幂。这是状态的标准处理方式。我们可以通过逻辑运算-或(OR |)来创建更多的组合状态,也可以用逻辑运算-与(AND &)来验证。所以对于通用32位系统,我们最多只有32种基础状态。
例如:#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)

Linux中进程状态的基础状态(TASK_RUNNINGTASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE__TASK_STOPPED__TASK_TRACEDEXIT_DEADEXIT_ZOMBIE),又分为两类: state(运行中的状态) 和 exit_state(退出状态)。

  • TASK_RUNNING

    表示进程处于执行状态或者就绪状态进程处于TASK_RUNNING时,并不意味着进程处于执行状态。只有被current指向的进程才是执行中的进程。

    系统中有一个运行队列(Run_Queue),存放处于TASK_RUNNING状态的进程。在系统执行调度进程时,会从该运行队列中选择符合条件的进程。current永远指向运行队列中的某个进程。

  • TASK_INTERRUPTIBLE

    表示进程处于阻塞状态,在等待某个资源或某个事件(比如等待Socket连接、等待信号量)。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒,转成TASK_RUNNING

    通过ps的指令我们可以看到进程列表的大部分的进程都是处于该状态下。在阻塞状态的进程,都位于系统的等待队列(Wait_Queue)中。

  • TASK_UNINTERRUPTIBLE

    表示进程处于阻塞状态。不同于TASK_INTERRUPTIBLE的是,该状态下的进程是不可中断的。不可中断,指的是进程不响应异步信号,指的并不是CPU不响应外部硬件的中断。

    Linux中阻塞状态的进程分为两种:可中断(TASK_INTERRUPTIBLE)和不可中断(TASK_UNINTERRUPTIBLE)的阻塞状态。

    • TASK_INTERRUPTIBLE: 如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度。也可以接受kill指令异步给的信号。
    • TASK_UNINTERRUPTIBLE: 往往因为硬件资源不能满足而阻塞,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。

    TASK_UNINTERRUPTIBLE状态存在的意义就在于,保护内核的某些处理流程是不被打断。如果进程响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。

    在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。

    应注意,部分工程师喜欢将进程设置为TASK_UNINTERRUPTIBLE,将导致一系列问题。当唤醒条件无法满足时候,进程是无法接受的异步指令的。最终只能通过重启系统解决。一方面,您需要考虑一些细节,因为不这样做会在内核端和用户端引入 bug。另一方面,您可能会生成永远不会停止的进程(被阻塞且无法终止的进程)。所以Linux Kernel2.6.25引进了新状态TASK_KILLABLE,解决这一问题。

  • TASK_KILLABLE

    TASK_KILLABLE = TASK_UNINTERRUPTIBLE + TASK_WAKEKILL

    表示进程处于阻塞状态。跟TASK_UNINTERRUPTIBLE类似,该下的进程是不可中断的。但又为了弥补TASK_UNINTERRUPTIBLE的缺陷(唤醒条件无法满足)。该状态下的进程只接受终止信号的异步信息,其余一概不接受。

  • __TASK_STOPPED

    表示进程被暂停了。通常当进程(TASK_UNINTERRUPTIBLE状态下的进程除外)接收到SIGSTOPSIGTSTPSIGTTINSIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。当进程接收到SIGCONT后,则恢复到TASK_RUNNING状态。

    对于该状态的进程,仍处于系统的运行队列中。但调度器并不会去执行该进程。

  • __TASK_TRACED

    表示进程被暂停了,且被Debugger程序监听跟踪。正在被跟踪,指进程暂停,等待调试进程对它进行其他操作。比如gdb中对跟踪进程标记断点后,进程运行到断点处代码,就会处于TASK_TRACED状态。

    TASK_TRACEDTASK_STOPPED都是表示进程被暂停。TASK_TRACED中的进程无法被SIGCONT信号唤醒。只能等到调试进程通过Ptrace系统调用执行PTRACE_CONTPTRACE_DETACH等操作(通过Ptrace系统调用的参数指定操作),或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。也就是TASK_TRACED其实是对调试进程的保护,避免被错误信号恢复执行状态

  • EXIT_ZOMBIE

    表示进程已经终止了。但还未被父进程通过wait()调用获知该进程的终止信息,又被称为Zombie的数据结构。

    该状态的进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。在进程退出后,会进入该状态,就是EXIT_ZOMBIE的进程在等待父进程采集终止信息中。
    该状态的进程不可通过kill清理。如果要清理该进程,要么等到采集时间超时,或者清除父进程。由于该进程还保留PID,过多的僵尸进程,会影响系统的调度效率。

  • EXIT_DEAD

    表示进程的最终状态。

  • 状态转移图

Linux状态转移图

3.3 内核进程信息 Address of Process

Linux中,线程有三种重要的数据结构,分别是内核栈、线程描述符和线程联合体。三者的关系,可以参考下图:

三者关系

进程相关源码链接(source/include/linux/sched.h

  • task_struct,是内核下进程描述符,包含了具体进程的所有信息。
    源码可以参考文件/include/linux/sched.h,截取部分代码如下。其中包含了thread_infostack。还将主体成员包含在randomized_struct_fields中,具体原因可以参考3.4节。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct task_struct {
    #ifdef CONFIG_THREAD_INFO_IN_TASK
    /*
    * For reasons of header soup (see current_thread_info()), this
    * must be the first element of task_struct.
    */
    struct thread_info thread_info;
    #endif
    /*
    * This begins the randomizable portion of task_struct. Only
    * scheduling-critical items should be added above here.
    */
    randomized_struct_fields_start

    void *stack;

    ...

    /*
    * New fields for task_struct should be added above here, so that
    * they are included in the randomized portion of task_struct.
    */
    randomized_struct_fields_end
    }
  • thread_info,是每个架构对进程(内核视角下)的具体实现信息。

    内核需要存储每个进程的控制块PCB信息,同时Linux要兼容不同的架构(x86ARM等),每个架构有自己独特的进程信息结构。所以需要有一个方式,将架构相关的内容与Linux进程管理通用部分解耦分离。

    用一种解耦的方式来描述进程, 就是task_struct, 而thread_info类型就保存了各架构自己需要的信息,同时我们还在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct

    task_struct结构里面通过CONFIG_THREAD_INFO_IN_TASK宏来控制是否有thread_info成员结构。

    thread_info结构体的内容和具体的体系架构有关,保存了为实现进入、退出内核态的特定架构的汇编代码段所需要访问的部分进程的数据,所以大多数架构采用了独立寄存器或栈寄存器来保存thread_info地址。

    下面是ARM体系下的thread_info结构,详见源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /*
    * low level task data that entry.S needs immediate access to.
    * __switch_to() assumes cpu_context follows immediately after cpu_domain.
    */
    struct thread_info {
    unsigned long flags; /* low level flags */
    int preempt_count; /* 0 => preemptable, <0 => bug */
    mm_segment_t addr_limit; /* address limit */
    struct task_struct *task; /* main task structure */
    __u32 cpu; /* cpu */
    __u32 cpu_domain; /* cpu domain */
    #ifdef CONFIG_STACKPROTECTOR_PER_TASK
    unsigned long stack_canary;
    #endif
    struct cpu_context_save cpu_context; /* cpu context */
    __u32 syscall; /* syscall number */
    __u8 used_cp[16]; /* thread used copro */
    unsigned long tp_value[2]; /* TLS registers */
    #ifdef CONFIG_CRUNCH
    struct crunch_state crunchstate;
    #endif
    union fp_state fpstate __attribute__((aligned(8)));
    union vfp_state vfpstate;
    #ifdef CONFIG_ARM_THUMBEE
    unsigned long thumbee_state; /* ThumbEE Handler Base register */
    #endif
    };

    thread_info结构中,嵌入了指向task_struct的成员task,方便我们通过该结构获取task_struct

  • current相关

    对于所有Linux兼容的架构,都必须实现currentcurrent_thread_info两个宏定义。

    • current_thread_info 可以获取当前执行进程的thread_info实例指针
    • current 给出当前进程进程描述符task_struct的地址,通常由current_thread_info确定
  • task_struct中的stack是个void类型的指针,指向进程(内核视角)对应的thread_union

  • thread_unionLinux内核栈线程描述信息thread_info组合成一个union

    详见源代码(source/include/linux/sched.h)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    union thread_union {
    #ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
    struct task_struct task;
    #endif
    #ifndef CONFIG_THREAD_INFO_IN_TASK
    struct thread_info thread_info;
    #endif
    unsigned long stack[THREAD_SIZE/sizeof(long)];
    };

    内核定义一个联合体thread_union用于将内核栈和thread_info存储在THREAD_SIZE大小的空间里,如果没有启用 CONFIG_ARCH_TASK_STRUCT_ON_STACK,那么联合体还会包含了一个task_struct结构。

  • THREAD_SIZEthread_union的空间大小。
    源码详见arch/(架构名称)/include/asm/thread_info.h, 该值与架构类型有关。

    例如,arm(arch/arm/include/asm/thread_info.h)中的定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #ifdef CONFIG_KASAN
    /*
    * KASan uses a lot of extra stack space so the thread size order needs to
    * be increased.
    */
    #define THREAD_SIZE_ORDER 2
    #else
    #define THREAD_SIZE_ORDER 1
    #endif
    #define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)

    从上面的定义可知,如果PAGE_SIZE的大小是4K的话,那么THREAD_SIZE就是8K

  • thread_union中的stack 指进程的内核栈

    进程在内核态运行时需要自己的堆栈信息(不是原用户空间中的栈), 因此Linux内核为每个线程都提供了一个内核栈kernel stack,

    用户态进程所用的栈,是在进程线性地址空间中;而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,并切换堆栈,那么在内核空间中使用的就是这个内核栈。

    注意task_struct中的stack并非指向内核栈,而是指向thread_union的地址。

    thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出。

  • sp 栈寄存器,用来存储当前栈顶地址。

    x86源码为例,arch/x86/include/asm/asm.h

    1
    2
    3
    4
    5
    6
    7
    /*
    * This output constraint should be used for any inline asm which has a "call"
    * instruction. Otherwise the asm may be inserted before the frame pointer
    * gets set up by the containing function. If you forget to do this, objtool
    * may print a "call without frame pointer save/setup" warning.
    */
    register unsigned long current_stack_pointer asm(_ASM_SP);

    sp寄存器是CPU栈指针,用来存放栈顶单元的地址。在 x86 系统中,堆栈从顶部开始,并朝着该内存区域的开头增长。从用户态切换到内核态后,进程的内核栈总是空的。因此,sp 寄存器指向这个堆栈的顶部。一旦数据写入堆栈,sp 的值就会递减。

    通过sp,可以拿到thread_info的地址,即在current点讲到的current_thread_info

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static inline struct thread_info *current_thread_info(void) __attribute_const__;

    static inline struct thread_info *current_thread_info(void)
    {
    return (struct thread_info *)
    (current_stack_pointer & ~(THREAD_SIZE - 1));
    }

    #define THREAD_SIZE_ORDER 1
    #define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)

    THREAD_SIZE8K,其二进制表示为0000 0000 0000 0000 0010 0000 0000 0000
    ~(THREAD_SIZE-1)的结果正好是1111 1111 1111 1111 1110 0000 0000 0000
    低十三位全为零,也就是sp的低十三位刚好被屏蔽,最终得到thread_info的地址。

    当内核态线程使用了更多的栈空间时,内核栈会溢出到thread_info部分,因此内核提供了kstack_end函数来判断是否在正确的内核栈空间里。参照源码/include/linux/sched/task_stack.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #ifndef __HAVE_ARCH_KSTACK_END
    static inline int kstack_end(void *addr)
    {
    /* Reliable end of stack detection:
    * Some APM bios versions misalign the stack
    */
    return !(((unsigned long)addr+sizeof(void*)-1) & (THREAD_SIZE-sizeof(void*)));
    }
    #endif

3.4 内核进程其他 Others

记录一些源码中的亮点:

  • randomized_struct_fields_start&randomized_struct_fields_end

    Linux内核在 4.13 中引入 Structure Randomization技术来随机化 task_struct 的大部分结构成员布局,用来防止利用结构体偏移来覆盖特定敏感字段(如函数指针)的内核攻击,有兴趣的可以看下这篇文章 Randomizing structure layout

    一个struct的内部数据存储是按照声明顺序的,因此黑客程序可以很容易得计算出一个关键值,比如跳转函数地址在内核一些结构体中的偏移。修改该偏移的值以后,就可以轻松控制内核执行自己的代码(内核态)。

    举例代码:

    1
    2
    3
    4
    5
    6
    struct critical_struct {
    int id;
    void (* fn) (void *data);
    void *data;
    ...
    };

    有这么一段内核源码,如果入侵程序获得了对应数据段的写权限,那么他可以通过$STRUCT_OFFSET + 0x04,也就是fn的函数地址修改为自己的代码地址,进而获得内核态的执行权限。如果混淆以后,就为这种侵入过程制造了难度。

3.5 进程相关指令 Process Commands

补充下进程相关的简单指令

  • ps

    • 描述: ps命令来自于英文词组”process status“的缩写,其功能是用于显示当前系统的进程状态。使用ps命令可以查看到进程的所有信息,例如进程的号码、发起者、系统资源使用占比(处理器与内存)、运行状态等等。帮助我们及时的发现哪些进程出现僵死不可中断等异常情况。
    • 语法格式: ps [参数]

    ps -eLf可以看到线程信息,其中PID指进程ID,LWP指线程ID(task_struct中的pid)。
    执行结果

  • top

    • 描述: Linux中常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,常用于服务端性能分析。
    • 语法格式: top [参数]

    默认top,上会显示对应的进程数
    执行结果
    top -H(也可以在进入界面后,再输入大写H),可以看到线程数。
    执行结果

  • htop

    • 描述: htopLinux系统中的一个互动的进程查看器,一个文本模式的应用程序(在控制台或者X终端中),需要ncurseshtop比较人性化。它可让用户交互式操作,支持颜色主题,可横向或纵向滚动浏览进程列表,并支持鼠标操作。
    • 语法格式: htop [参数]

    要在htop中启用线程查看,开启htop,然后按F2来进入htop的设置菜单。选择Setup栏下面的Display options,然后勾选Tree viewShow custom thread names选项。按F10退出设置。

    执行结果

推荐几个不错的文档网站:

四、总结 Summary

所以进程大致内容就是这些,也是操作系统最基础的抽象概念。简单来说就是运行的程序。接下来会继续阐述进程的其他机制,如系统调用和调度算法。从而了解操作系统是如何实现CPU虚拟化的。

如内容有错误,或不足,请留言或者直接联系我。万分感谢指正。

作者

MinRam

发布于

2022-04-13

更新于

2022-07-17

Licensed under

评论