进程管理基础学习笔记 - 1.概述

本文深入探讨了Linux内核的进程管理,包括进程与线程的区别、进程创建标志、进程数据结构、生命周期,以及进程调度的策略、优先级、就绪队列和抢占机制。重点阐述了各种调度器、调度策略和进程优先级的计算,同时详细解释了进程抢占的触发时机和条件。此外,还介绍了jiffies和HZ在系统计时中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 前言

本专题我们开始学习进程管理部分,本文为概述部分。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。

kernel版本:5.10
平台:arm64

2.进程相关

进程和线程的区别

  • 进程
    1.进程是处于执行期的程序,进程=程序+执行
    2.进程是资源封装的单位,拥有独立的资源空间,包含很多资源:打开的文件、挂起的信号量、内存管理、处理器状态、一个或多个执行线程或数据段等;
    3.进程通常通过fork系统调用来创建;
    4.新创建的进程可以通过exec创建地址空间(用户栈初始化等),并载入新的可执行程序
    5.进程退出可以自愿退出或非自愿退出
  • 线程
    1.线程是轻量级进程,是操作系统调度的最小单位
    2.一个进程可以有多个线程线程共享进程的资源空间
    3.线程通过clone方法来创建,会确定哪些资源与父进程共享,哪些资源线程独享

获取当前进程

在内核态,ARM64运行级别为EL1,SP_EL0寄存器在EL1上下文没有使用,利用SP_EL0寄存器存放当前进程描述符task_struct的地址。

创建进程的标志

低字节指定子进程结束时发送给父进程的信号代码,一般为SIGCHLD信号,剩余3字节给一clone标志组用于编码

/*
 * cloning flags:
 */
#define CSIGNAL         0x000000ff      /* signal mask to be sent at exit */
/*共享内存描述符和所有的页表*/
#define CLONE_VM        0x00000100      /* set if VM shared between processes */
/*共享根目录和当前工作目录*/
#define CLONE_FS        0x00000200      /* set if fs info shared between processes */
/*共享打开文件表*/
#define CLONE_FILES     0x00000400      /* set if open files shared between processes */
/*共享信号处理程序表、阻塞信号表和挂起信号表*/
#define CLONE_SIGHAND   0x00000800      /* set if signal handlers and blocked signals shared */
/*
 * 在创建子进程时启用Linux内核的完成量机制,wait_for_completion()会使父进程进入睡眠状态,
 * 直到子进程调用execve()/exit()释放内存资源
 */
#define CLONE_PIDFD     0x00001000      /* set if a pidfd should be placed in parent */
/*如果父进程被跟踪,子进程也被跟踪*/
#define CLONE_PTRACE    0x00002000      /* set if we want to let tracing continue on the child too */
/*vfork系统调用时设置*/
#define CLONE_VFORK     0x00004000      /* set if the parent wants the child to wake it up on mm_release */
/*设置子进程的父进程为调用进程的父进程*/
#define CLONE_PARENT    0x00008000      /* set if we want to have the same parent as the cloner */
/*把子进程插入到父进程同一线程组中,并迫使子进程共享父进程的信号描述符,此标志为true则必须设置CLONE_SIGHAND*/
#define CLONE_THREAD    0x00010000      /* Same thread group? */
/*当clone需要自己的命名空间(即子进程自己的已挂载文件系统视图)时设置这个标志*/
#define CLONE_NEWNS     0x00020000      /* New mount namespace group */
/*共享System V IPC取消信号量的操作*/
#define CLONE_SYSVSEM   0x00040000      /* share system V SEM_UNDO semantics */
 /*为子进程创建新的线程局部存储段(TLS)*/
#define CLONE_SETTLS    0x00080000      /* create a new TLS for the child */
/*设置父进程的TID*/
#define CLONE_PARENT_SETTID     0x00100000      /* set the TID in the parent */
/*
 * 如果该标志被设置,则内核建立一种触发机制,用在子进程要退出或要开始执行新程序时
 * 这些情况下,内核将消除由参数ctid所指向的用户态变量,并唤醒等待这个事件的任何进程
 */
#define CLONE_CHILD_CLEARTID    0x00200000      /* clear the TID in the child */
/*遗留标志,内核忽略*/
#define CLONE_DETACHED          0x00400000      /* Unused, ignored */
 /*设置这个标志使得CLONE_PTRACE标志失去作用*/
#define CLONE_UNTRACED          0x00800000/*set if the tracing process can't force CLONE_PTRACE on this clone*/
/*设置子进程TID*/
#define CLONE_CHILD_SETTID      0x01000000      /* set the TID in the child */
#define CLONE_NEWCGROUP         0x02000000      /* New cgroup namespace */
/*为子进程创建新的utsname命名空间*/
#define CLONE_NEWUTS            0x04000000      /* New utsname namespace */
/*为子进程创建新的ipc命名空间*/
#define CLONE_NEWIPC            0x08000000      /* New ipc namespace */
/*为子进程创建新的user命名空间,包括User id,Group id*/
#define CLONE_NEWUSER           0x10000000      /* New user namespace */
/*为子进程创建新的pid命名空间*/
#define CLONE_NEWPID            0x20000000      /* New pid namespace */
/*为子进程创建新的network命名空间*/
#define CLONE_NEWNET            0x40000000      /* New network namespace */
/*复制IO上下文*/
#define CLONE_IO                0x80000000      /* Clone io context */

进程的3个数据结构

进程是资源封装的单位,linux有3个数据结构来组织进程描述符:
(1) 链表:所有task_struct形成一张链表
(2) 树:所有task_struct指向它的父、兄弟、子,所有task_struct形成树,pstree可以看出进程树情况,父进程类似于子进程的监视器,子进程死父进程通过查看子进程的尸体(task_struct)可以知道子进程死亡原因,并将其清理
(3) 哈希表,通过PID可以快速检索出task_struct

进程的生命周期

在这里插入图片描述

  • TASK_RUNNING对应运行和就绪,进程要么在CPU上执行,要么准备执行
  • TASK_INTERRUPTIBLE:浅度睡眠,除了等IO ready唤醒,信号也可以唤醒,进程状态修改为TASK_RUNNING
  • TASK_UNINTERRUPTIBLE:深度睡眠,一定要等到IO ready唤醒,不能被异步信号打断的挂起状态, 对于不可中断的执行流程会用到这个状态
  • TASK_STOPPED:进程的执行被暂停。当进程收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU信号会暂停
  • TASK_ZOMBLE: 进程变为僵尸态后,所有的资源都被free 了,只剩下task_struct了。因为linux认为应用态有很多不可以预知的行为导致进程挂掉,因此它会在进程挂掉时自动释放进程资源,只保留task_struct来给父进程看原因,一旦父进程通过waitpid返回则task_struct也被释放。

3. 进程调度相关

通过调度策略来选用调度器,每个调度器都定义了一个调度类,每个调度类中实现了本调度器的关键算法。
所有的调度类链接在一起并通过优先级排列,大内核调度器(scheduler)调度的时候将按照调度类的优先级进行调度。

调度器

调度器是一个操作系统的核心部分。可以比作是CPU时间的管理员。调度器主要负责选择某些就绪的进程来执行。不同的调度器根据不同的方法挑选出最适合运行的进程。目前Linux支持的调度器主要包括如下:
Stop scheduler、Deadline scheduler、RT scheduler、CFS scheduler、Idle scheduler

将调度器公共的部分抽象,使用struct sched_class结构体描述一个具体的调度类,他们都使用struct sched_class来定义调度类,系统核心调度代码会通过struct sched_class结构体的成员调用具体调度类的核心算法。

struct sched_class {
        u8 state;
        u8 idx;
        struct ch_sched_params info;
        enum sched_bind_type bind_type;
        struct list_head entry_list;
        atomic_t refcnt;
};

针对以上每一个调度器定义了一个调度类,有5个调度类。对于每一个调度类,系统中有明确的优先级概念。

#include/asm-generic/vmlinux.lds.h
/*
 * The order of the sched class addresses are important, as they are
 * used to determine the order of the priority of each sched class in
 * relation to each other.
 */
#define SCHED_DATA                              \
        STRUCT_ALIGN();                         \
        __begin_sched_classes = .;              \
        *(__idle_sched_class)                   \
        *(__fair_sched_class)                   \
        *(__rt_sched_class)                     \
        *(__dl_sched_class)                     \
        *(__stop_sched_class)                   \
        __end_sched_classes = .;

优先级从高到低排列为:
stop_sched_class -> dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class
这个顺序是在如上链接脚本中定义的。

/* Defined in include/asm-generic/vmlinux.lds.h */
extern struct sched_class __begin_sched_classes[];
extern struct sched_class __end_sched_classes[];

#define sched_class_highest (__end_sched_classes - 1)
#define sched_class_lowest  (__begin_sched_classes - 1)

#define for_class_range(class, _from, _to) \
        for (class = (_from); class != (_to); class--)

#define for_each_class(class) \
        for_class_range(class, sched_class_highest, sched_class_lowest)	

通过for循环先处理最高优先级的调度类,调用它的调度算法找到下个调度实体,再处理次高优先级的调度类,调用它的调度算法找到下个调度体,直到处理最低优先级的调度类。

注:这部分的代码分析可参考 pick_next_task部分
关于调度类参考http://www.wowotech.net/process_management/447.html

补充:
1.O(N)调度器
1992年发布,每次调度前遍历所有的进程,选出优先级最高的进程作为下一个调度进程,算法复杂度O(N)
2.O(1)调度器
a.应用于内核2.6.23,是对O(1)调度器的优化,算法复杂度O(1)
b.通过为每个CPU维护一组进程优先级队列,每个队列对应一个优先级,并对应一张位图
c.通过查找每个优先级对应的位图看是否有就绪的进程
d.对交互式进程反应缓慢,且对NUMA支持不完善
3.CFS调度器
Con Kolivas提出了RSDL(楼梯调度算法)来实现公平性,RedHat公司的Ingo Molnar借鉴RSDL的思想提出一个CFS调度算法。

调度策略

/*
 * Scheduling policies
 */
#define SCHED_NORMAL            0
#define SCHED_FIFO              1
#define SCHED_RR                2
#define SCHED_BATCH             3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE              5
#define SCHED_DEADLINE          6

用户空间程序可以通过sched_setscheduler来设定用户进程的调度策略。其中:

  • SCHED_NORMAL和SCHED_BATCH为CFS调度器的调度策略
  • SCHED_FIFO和SCHED_RR为realtime调度器的调度策略
  • SCHED_IDLE为dle调度器的调度策略
  • SCHED_DEADLINE为deadline调度器的调度策略

进程优先级

进程分类(按调度策略)调度器 调度策略优先级(normal_prio)
实时进程
Deadline
SCHED_DEADLINE
-1
Realtime
SCHED_FIFO / SCHED_RR
0 ~ 99
普通进程
CFS
SCHED_NORMAL / SCHED_BATCH
100 ~ 139

通过调度器一节和调度策略一节可知,每一个进程都属于某一个调度器的调度队列,每个调取器(或进程)都有自己的调度策略,依据采用的调度策略可将进程划分为两类:实时进程和普通进程。

  1. 实时进程
  • deadline进程
    进程采用deadline调度器,是优先级最高的实时进程,采用SCHED_DEADLINE实时调度策略,进程的调度优先级为-1;
  • realtime进程
    进程采用Realtime调度器,是普通实时进程,采用SCHED_FIFO或SCHED_RR的实时调度策略,进程的调度优先级为:0~99;
  1. 普通进程
    普通进程是采用非SCHED_FIFO和SCHED_RR的非实时调度策略的进程,一般采用CFS调度器,进程的调度优先级为:100~139;
    (1).普通进程用户空间有一个传统的nice值(范围为-20~19,默认为0),映射到普通进程即100-139的进程,用于体现不同优先级的轮转公平性,普通进程优先级只体现在配额和唤醒后的抢占上
    (2).普通进程的nice值的范围为-20~19,默认nice值为0,nice值的含义类似于优先等级,nice值越高,优先级越低
    (3) nice值为0的权重为1024,其它nice值的权重通过查表获取,内核预先计算了一个表prio_to_weight[40],每个成员分别对应nice值分别为-20~19的权重

注:
1.进程的优先级分为静态优先级(static_prio)、动态优先级(prio)、标准优先级(normal_prio),实时优先级(rt_priority);
2.其中静态优先级和动态优先级只有普通进程才有;实时优先级为实时进程所特有;标准优先级为普通进程和实时进程都有
3.静态优先级(static_prio)是在进程启动时被初始化,数值越小,优先级越高,可以通过设置nice值来调整;
4.动态优先级(prio)是在进程执行中根据进程的行为(如 CPU 使用率)动态调整的;
5.实时优先级(rt_priority)也是在实时进程启动时设定,数值越小,优先级越高,可通过chrt 命令行工具来修改;
6.标准优先级(normal_prio)是为进程提供一个“标准化”的优先级值,供调度器使用
7.对普通进程,初始时静态优先级=动态优先级=标准优先级,通过set_user_nice函数可修改静态优先级,进而通过effective_prio函数修改动态优先级;
注:初始时普通进程的动态优先级初始化为父进程的标准优先级
8.对实时进程,通过sched_setparam可修改实时进程优先级(rt_priority),进而通过normal_prio函数修改标准优先级= MAX_RT_PRIO-1 - p->rt_priority;
关于进程优先级可参考:https://blog.youkuaiyun.com/gatieme/article/details/51719208

进程的权重

  • prio_to_weight[40]
    记录不同的nice值对应的权重,nice越低权重越高,nice越高权重越低。下标值记录了nice从-20到19对应的权重
    注:只针对优先级为100~139的普通进程

  • prio_to_wmult[40]
    记录不同nice值的权重的倒数乘以2^32即inv_weight,计算公式如下,其中inv weight是inverse weight的缩写,代表权重被翻转了,用于计算方便
    在这里插入图片描述
    引入此的目的,如:CFS计算虚拟运行时间:
    在这里插入图片描述
    其中: vruntime表示进程虚拟的运行时间,delta_execi表示实际的运行时间,nice_0_weight表示nice为0的权重值,weight表示该进程的权重值。
    为了计算方便,避免浮点运算,引入了inv_weight,因此vruntime可以表示为:
    在这里插入图片描述
    内核提供了set_load_weight(struct task_struct *p)来查询如上两张表,然后将结果存放到p->se.load数据结构,即struct load_weight

就绪队列

  1. 系统中每个CPU都会有一个全局的就绪队列(cpu runqueue),使用struct rq结构体描述,它是per-cpu类型,即每个cpu上都会有一个struct rq结构体
  2. 每一个调度类也有属于自己管理的就绪队列。例如,struct cfs_rq是CFS调度类的就绪队列,管理就绪态的struct sched_entity调度实体

进程切换

进程切换分为自愿(VOLUNTARY)与强制切换(INVOLUNTARY)

  1. 自愿切换(Voluntary)
    进程运行不下去了:比如因为要等待IO完成,或者等待某个资源、某个事件
  2. 强制切换(Involuntary)
    (1)进程还在运行,但内核不让它继续使用CPU了(发生了抢占):
    比如进程的时间片用完了,或者优先级更高的进程来了,所以该进程必须把CPU的使用权交出来
    (2)进程还可以运行,但它自己的算法决定主动交出CPU给别的进程:
    用户程序可以通过系统调用sched_yield()来交出CPU,内核则可以通过函数cond_resched()或者yield()来做到

在支持抢占调度的系统里,进程调度与进程抢占同义?

参考:http://linuxperf.com/?p=209

进程抢占

|- - -抢占的过程

  1. 第一步触发抢占
    给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换
  2. 第二步执行抢占
    在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占

|- - -抢占的时机

  1. 触发抢占的时机
    每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched(),触发抢占的函数是resched_curr()。
    (1). 周期性的时钟中断
    时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占,这里对deadline调度类、rt调度类、CFS调度类分别进行判断触发抢占
    (2). 唤醒进程的时候
    当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。
    相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。
    (3). 新进程创建的时候
    如果新进程的优先级高于CPU上的当前进程,会触发抢占,跟唤醒进程类似,它会通过check_preempt_curr()检查是否触发抢占
    (4). 进程修改nice值的时候?
    如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()
    (5). 进行负载均衡的时候
    在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。
    比如CFS类在load_balance()中触发抢占;RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占
  2. 执行抢占的时机
    (1).执行User Preemption(用户态抢占)的时机
    a. 从系统调用(syscall)返回用户态时;
    b. 从中断返回用户态时
    (2).执行Kernel Preemption(内核态抢占)的时机
    a. CONFIG_PREEMPT_NONE=y
    不允许内核抢占。这是SLES的默认选项;
    b. CONFIG_PREEMPT_VOLUNTARY=y
    在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项;
    c. CONFIG_PREEMPT=y
    允许完全内核抢占。
    -中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占
    -当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候,并且当前进程设置了TIF_NEED_RESCHED标志则执行抢占

注:对于某些情况下,不需要触发抢占,即不需要设置TIF_NEED_RESCHED标志,直接通过调用schedule执行抢占,如:mutex, semaphore, waitequeue等执行阻塞操作时会直接调用schedule执行抢占

|- - -抢占的条件

preempt_count == 0 && !irq_disable()

jiffies和HZ

  • jiffies
    记录了系统启动以来,系统定时器已经触发的次数。内核每秒钟将jiffies变量增加HZ次。
    对于HZ值为100的系统,1个jiffy等于10ms,而对于HZ为1000的系统,1个jiffy仅为1ms
  • HZ
    系统定时器能以可编程的频率中断处理器。此频率即为每秒的定时器节拍数,对应着内核变量HZ。
    选择合适的HZ值需要权衡。HZ值大,定时器间隔时间就小,因此进程调度的准确性会更高;
    HZ值越大也会导致开销和电源消耗更多,因为更多的处理器周期将被耗费在定时器中断上下文中

参考文档

  1. 奔跑吧,Linux内核
  2. ULK
  3. ULA
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值