一步步理解Linux进程(4)--等待队列和进程切换

本文介绍了Linux内核中的等待队列机制,包括其概念、使用方法及内核实现细节。详细阐述了等待队列头与等待队列项的数据结构,并通过示例展示了如何在实际代码中使用等待队列。

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

0. 说明

作者:Gao Peng<gaopenghigh@gmail.com>
本文章由Gao Peng编写,转载请注明出处。



1. 等待队列

假如一个进程去读一个磁盘上的文件,但是这个磁盘反应比较慢(相对于CPU来说),这是进程不能马上读取到数据,于是被阻塞了。此时内核一般会把这个进程的状态设置为睡眠TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,然后把这个进程放入等待队列里面,直到数据读取出来了才再次运行。

等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。



2. 等待队列的使用

使用等待队列
加入我们定义了一个函数write_to_buffer()把数据写入一个缓存区,但当这个缓存区已满时我们希望这个函数阻塞住,直到缓存区有空间。缓存区是否满由一个变量is_full表示。则大致的实现如下:
/* 建立并初始化一个等待队列头,当然也可以选用已经存在的等待队列 */
DECLARE_WAIT_QUEUE_HEAD(wq);
/* 建立一个等待队列项 */
wait_queue_t my_wait;
/* 用当前进程的进程描述符初始化这个等待队列项 */
init_wait(&my_wait);
/* 将等待队列项添加到等待队列头中,并设置进程的状态 */
prepare_to_wait(&wq, my_wait, state);
/* 调用schedule,告诉内核调用其他的进程执行 */
schedule();
/* schedule返回,完成后续清理工作 */
finish_wait();

等待队列由“等待队列头(struct wait_queue_head_t)”和“等待队列项(struct wait_queue_t)”组成。
schedule函数是内核的进程调度函数,当它被调用是,内核会按照一定的调度算法选择出下一个执行的进程,然后进行进程切换的操作。schedule返回后,说明这个进程醒来了(需要的条件成立了)并且被内核选中开始执行了。

等待队列的操作
2.6内核提供如下关于等待队列的操作:

a. 定义和初始化“等待队列头”
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
或者调用宏:
DECLARE_WAIT_QUEUE_HEAD(my_queue);

b. 定义并初始化“等待队列项”
DECLARE_WAITQUEUE(name, tsk);
其中name是类型为“struct wait_queue_t”的指针,tsk类型为“struct task_struct”的指针

c.添加/移除“等待队列项”
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

d. 等待事件
wait_event(wq, condition); /* 不可中断的等待 */
wait_event_interruptible(wq, condition) /* 可中断的等待 */
wait_event_timeout(wq, condition, timeout) /* 带超时返回的等待 */
wait_event_interruptible_timeout(wq, condition, timeout) /* 可中断并超时返回的等待 */

e. 唤醒队列
wake_up(wait_queue_head_t *q) ; /* 唤醒q上所有等待的进程 */
wake_up_interruptible(wait_queue_head_t *q); /* 只唤醒q上执行可中断休眠的进程 */



3. 等待队列的内核实现

等待队列头
等待队列由双向链表实现,每一个等待队列都有一个等待队列头(wait queue head), 其类型是名为wait_queue_head_t的数据结构:
/* <linux/wait.h> */
struct __wait_queue_head {
    spinlock_t lock;              /* 自旋锁 */
    struct list_head task_list;   /* 指向链表 */
};
typedef struct __wait_queue_head wait_queue_head_t;

由于等待队列是由中断处理程序和主要内核函数修改的,所以必须对链表进行保护,防止它被同时访问,这样的同步是通过wait_queue_head结构中的lock自旋锁实现的。task_list是等待进程链表的头。

等待队列项
等待队列项的数据结构是wait_queue_t,定义如下:
/* linux/wait.h> */
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
    unsigned int flags;
#define WQ_FLAG_EXCLUSIVE   0x01
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};

各个字段的解释:
private:
等待队列项代表一个睡眠的进程。在wait_queue_t结构中,这个进程的进程描述符的地址存放在private字段中。
flags:
有的时候,如果有两个或者多个进程等待互斥访问某一要释放的资源,这时候,就只需要唤醒其中的一个进程,不然全部唤醒的话,它们又会去抢着用那个资源,最后的结果是只有一个进程抢到,其它进程又要去睡觉,白白醒来一次耗费资源。为了解决这样的问题,内核把睡眠进程分为两种:互斥进程(等待队列元素中flags字段为1)和非互斥进程(flags字段为0),互斥进程由内核有选择地唤醒,而非互斥进程则由内核一次全部唤醒。
func:
wait_queue_t中,func字段标识等待队列中的睡眠进程应该用什么方式来唤醒

等待队列结构图
整个等待队列的结构如下图所示:


等待队列操作的实现
下面我们看几个等待队列的主要操作的实现:

a. DECLARE_WAIT_QUEUE_HEAD(name)宏
/* SRC/include/linux/wait.h */
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {               \                   
    .lock       = __SPIN_LOCK_UNLOCKED(name.lock),      \                       
    .task_list  = { &(name).task_list, &(name).task_list } }                    
                                                               
#define DECLARE_WAIT_QUEUE_HEAD(name) \                                         
    wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

b. init_wait(wait)宏
#define init_wait(wait)                         \                               
    do {                                \                                       
        (wait)->private = current;              \                               
        (wait)->func = autoremove_wake_function;        \                       
        INIT_LIST_HEAD(&(wait)->task_list);         \                           
        (wait)->flags = 0;                  \                                   
    } while (0)

c.ECLARE_WAITQUEUE(name, tsk)宏
#define __WAITQUEUE_INITIALIZER(name, tsk) {                \                   
    .private    = tsk,                      \                                   
    .func       = default_wake_function,            \                           
    .task_list  = { NULL, NULL } }                                              
                                                                                
#define DECLARE_WAITQUEUE(name, tsk)                    \                       
    wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

d. wait_event(wq, condition)宏
#define __wait_event(wq, condition)                     \                       
do {                                    \                                       
    DEFINE_WAIT(__wait);                        \                               
                                    \                                           
    for (;;) {                          \                                       
        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    \               
        if (condition)                      \                                   
            break;                      \                                       
        schedule();                     \                                       
    }                               \                                           
    finish_wait(&wq, &__wait);                  \                               
} while (0)

#define wait_event(wq, condition)                   \                           
do {                                    \                                       
    if (condition)                          \                                   
        break;                          \                                       
    __wait_event(wq, condition);                    \                           
} while (0)




4. 进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。

进程上下文和硬件上下文
一般的程序在用户空间执行,当一个程序执行了系统调用或者出发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间继续执行。

每个进程可以拥有自己的地址空间,但所有进程必须共享CPU寄存器。所以,在恢复一个进程执行之前,必须确保每个寄存器装入了挂起进程时的值。这组必须装入寄存器的数据就称为硬件上下文(hardware context)。

任务状态段
TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。内核为每个CPU创建一个TSS。每个TSS有它自己的8字节的任务状态段描述符(Task State Segment Descriptor, TSSD)。

TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR(任务寄存器)所指向的TSSD代表的TSS中,然后,下一任务的TSS的TSSD被装入TR;最后,从TR所指定的TSSD代表的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。Linux创建的TSSD存放在全局描述符表(GDT)中,GDT的基地址存放在每个CPU的gdtr寄存器中。每个CPU的tr寄存器包含相应TSS的TSSD选择符,事实上还存了两个隐藏的非编程字段:TSSD的Base字段(指向对应的TSS的地址)和Limit字段,这样CPU就可以直接对TSS寻址而不用从GDT中检索TSS的地址。

在每次进程切换时,被替换进程的硬件上下文必须保存在某个地方,这个地方就是进程描述符的类型为thread_struct的thread字段:
/* <linux/sched.h> */
struct task_struct {
...
/* CPU-specific state of this task */
    struct thread_struct thread;
...
}

这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如eax、ebx等等这些通用寄存器,它们的值保留在内核堆栈中。

执行进程切换
本质上说,每个进程切换由两步组成:
1. 切换页全局目录以安装一个新的地址空间;
2. 切换内核态堆栈和硬件上下文。
第一个步骤我们暂时不讨论,我们先来简单看看第二步的具体过程。这个过程由switch_to宏执行,该宏有3个参数:prev,next和last,prev表示当前的进程描述符,next表示准备切换到的进程的进程描述符,last表示这次切换发生后eax寄存器的值所存的地方(注意,在进程切换时,eax、ebx等等这些通用寄存器的值是不变的)。last有什么意义呢?我们看一个例子:进程A切换到进程B,再切换到C进程,最后又从进程C切换回进程A。切换回A后,寄存器和thread字段的值都是当初冻结A时的值,也就是prev=A,next=B,这样我们就找不到C了,而找到C在进程调度的过程中很有用,我们需要有个地方来存当前进程到底是从哪个进程切换过来的,这就是last的作用。具体过程如下(由switch_to宏执行):
a. 把当前进程A的prev的值存入eax寄存器,eax=A;
b. 进程切换到B,prev=B,把eax的值读入到last,last=A;
c. 此时需要切换到进程C,把prev的值存到eax,eax=B;
d. 进程切换到C,prev=C,把eax的值读入到last,last=B;
e. 此时需要切换回进程A,把prev的值存入eax,eax=C;
f. 切换到进程A,prev=A,把eax的值读入last,last=C; 这样在进程A中,通过last就可以直达它是从哪个进程切换过来的。
进程切换示例图:

具体的切换由一系列的各种寄存器的值的切换组成,在此我们不做深入研究。


参考资料:
《深入理解Linux内核》
《Linux内核设计与实现》
Linux内核等待队列机制介绍 http://soft.chinabyte.com/os/81/12374081.shtml
<think>好的,我现在需要帮助用户解决Linux服务器CPU负载过高的问题。用户想要具体的排查命令思路,所以我得整理一个系统化的流程,确保他们能一步步定位问题。 首先,我应该从整体负载情况开始。用户可能已经注意到服务器变慢,但需要确认CPU负载是否真的过高。使用uptime或top命令查看平均负载,这三个数值分别代表1分钟、5分钟、15分钟的平均负载。如果这些值超过CPU核心数,说明存在高负载问题。比如,双核CPU的话,如果负载超过2,可能就有问题。 接下来需要确定是用户进程还是系统进程导致的高负载。top命令是一个很好的起点,因为它能实时显示各个进程的CPU占用情况。在top界面中,按Shift+P可以按CPU使用率排序,找出占用高的进程。这时候,用户可能不知道哪些进程是正常的,所以需要提醒他们注意异常的进程名或高占用的未知进程。 如果发现某个进程占用过高,可能需要进一步分析。这时候可以用pidstat命令,指定进程ID采样间隔,比如pidstat -p <PID> 1 5,这样可以持续观察该进程的CPU使用情况。此外,strace命令可以跟踪进程的系统调用,帮助确定是否有频繁的系统调用或阻塞问题。不过要注意,strace可能会影响性能,生产环境需谨慎使用。 除了用户空间的进程,还要考虑内核态的CPU使用情况。top命令中,如果sy(系统CPU时间)过高,说明内核态任务繁重。这时候需要使用perf工具来分析,比如perf top查看热点函数,或者记录性能数据后离线分析。另外,mpstat -P ALL 1可以查看每个CPU核心的使用情况,帮助判断是否有单个核心被占满的情况。 中断上下文切换也是可能的因素。使用vmstat 1查看上下文切换(cs列)中断(in列),如果数值异常高,可能需要进一步分析。比如,用pidstat -w查看进程的上下文切换情况,或者检查硬件中断是否过多,使用cat /proc/interrupts查看中断分布。 如果怀疑是I/O等待导致CPU负载高,top中的wa(等待I/O时间)值高的话,需要检查磁盘I/O。iostat -x 1可以查看设备的I/O使用率等待时间。同时,iotop可以找出I/O高的进程。如果磁盘I/O确实很高,可能需要优化查询或增加缓存。 对于多线程应用,如果存在线程竞争或死锁,可能导致CPU空转。这时候可以用pstree -p <PID>查看线程情况,或者用top -H -p <PID>查看线程级别的CPU使用。结合jstack(对于Java应用)可以分析线程状态,但要注意用户的应用类型。 另外,短期故障可以使用dmesg | grep -i kill检查是否有OOM Killer终止进程,这可能导致资源争用。系统日志/var/log/messages也可能有相关错误信息。 最后,如果用户没有安装高级工具,可以用ps aux --sort=-%cpu | head快速找出占用高的进程,或者检查/proc/stat/proc/<PID>/stat中的CPU时间信息。 总结下来,步骤应该是:确认负载情况→定位高CPU进程→分析进程行为→检查内核中断→I/O分析→线程分析→日志检查。需要提醒用户在生产环境中谨慎使用某些诊断工具,避免影响服务,同时考虑历史数据对比,比如sar工具可以提供历史性能数据。如果用户是新手,可能需要更详细的命令解释注意事项,比如在试运行环境测试,避免直接在生产环境执行可能影响服务的命令。</think>以下是一个系统化的CPU负载问题排查指南,包含诊断思路、常用命令及解决方案: --- ### **一、快速诊断流程图** ```mermaid graph TD A[发现CPU负载高] --> B{查看整体负载} B --> |1.uptime检查| C[确认负载阈值] B --> |2.top/htop| D[定位高CPU进程] D --> E{分析进程行为} E --> |用户态高| F[应用代码/配置优化] E --> |内核态高| G[系统调用/驱动分析] D --> H{检查关联指标} H --> I[上下文切换/中断] H --> J[I/O等待] H --> K[锁竞争/死锁] ``` --- ### **二、具体排查步骤** #### **1. 确认CPU负载情况** ```bash # 查看平均负载(1/5/15分钟) $ uptime 12:30:45 up 15 days, 2:30, 2 users, load average: 3.25, 2.80, 2.40 # 显示CPU核心数 $ grep 'model name' /proc/cpuinfo | wc -l 4 ``` **判断标准**:若1分钟负载 > CPU核心数*0.7 需关注 --- #### **2. 定位高CPU进程** ```bash # 实时进程监控(按P排序CPU) $ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1234 mysql 20 0 28.765g 4.823g 12384 S 198.3 15.2 100:30.12 /usr/sbin/mysqld ``` **关键列解析**: - `%CPU`:进程CPU占用率 - `S`:进程状态(R=运行中, S=睡眠) - `TIME+`:累计CPU时间 --- #### **3. 深入分析进程行为** ```bash # 查看进程详细状态 $ pidstat -p 1234 1 5 # 每秒采样,共5次 # 跟踪系统调用 $ strace -p 1234 -c # 统计系统调用(生产环境慎用) $ perf top -p 1234 # 分析函数级CPU消耗 ``` --- #### **4. 检查内核态CPU使用** ```bash # 查看CPU时间分布 $ mpstat -P ALL 1 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 0 12.34 0.00 3.45 0.12 0.01 0.23 0.00 0.00 0.00 83.85 # 查看软中断分布 $ cat /proc/softirqs ``` **关键指标**: - `%sys`高:内核空间消耗大(常见于频繁系统调用) - `%soft`高:软中断处理耗时(网络包处理等) --- #### **5. 分析上下文切换** ```bash # 查看全局上下文切换 $ vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 3 0 0 1680044 292136 4582308 0 0 12 24 234 4567 20 15 65 0 0 # 查看进程级上下文切换 $ pidstat -w -p 1234 1 ``` **判断标准**: - `cs`(context switch) > 100,000/秒需关注 - 自愿切换多:I/O等待 - 非自愿切换多:CPU竞争 --- #### **6. 检查I/O等待** ```bash # 查看I/O压力 $ iostat -x 1 Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util vda 0.12 2.34 4.56 234.56 0.00 0.12 0.00 4.76 0.23 1.23 0.01 38.15 100.23 0.12 0.34 # 查看进程I/O $ iotop -oP ``` --- ### **三、常见问题场景及解决** #### **场景1:用户态CPU高** - **特征**:`%usr`高,某个进程持续占用CPU - **解决方案**: 1. 分析进程堆栈:`gdb -p <PID>` → `thread apply all bt` 2. Java应用:`jstack <PID> > stack.log` 分析线程状态 3. Python应用:`py-spy top -p <PID>` #### **场景2:内核态CPU高** - **特征**:`%sys`高,常见于频繁系统调用 - **解决方案**: 1. 使用`perf record -g -p <PID>`记录性能数据 2. 检查系统调用频率:`strace -c -p <PID>` 3. 优化文件操作(减少`open/close`调用) #### **场景3:大量软中断** - **特征**:`%soft`高,常见于网络密集型应用 - **解决方案**: 1. 检查网卡多队列:`ethtool -l eth0` 2. 启用RSS/RPS:`/sys/class/net/eth0/queues/` 3. 调整`net.core.netdev_budget`参数 --- ### **四、高级诊断工具** | 工具 | 用途 | 示例命令 | |---------------|-------------------------------|------------------------------| | `perf` | 函数级性能分析 | `perf record -g -p <PID>` | | `bpftrace` | 动态内核追踪 | `bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'` | | `sar` | 历史性能数据分析 | `sar -u -f /var/log/sa/sa15` | | `numastat` | NUMA内存分析 | `numastat -p <PID>` | --- ### **五、预防建议** 1. **监控告警**:部署Prometheus+Node Exporter监控`node_load1`指标 2. **压测预案**:使用`stress-ng`模拟高负载场景进行测试 3. **内核参数调优**: ```bash # 减少进程切换开销 echo 'kernel.sched_migration_cost_ns=5000000' >> /etc/sysctl.conf # 调整进程调度策略 echo 'kernel.sched_child_runs_first=0' >> /etc/sysctl.conf ``` 通过以上步骤,90%以上的CPU负载问题可在15分钟内定位到根本原因。建议生产环境操作前先在测试环境验证诊断方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值