linux 中平均负载统计时包括了 D 状态的线程

linux 中平均负载的统计中包括了 TASK_RUNNING 的线程以及 TASK_UNINTERRUPTIBLE 的线程。

1 D 状态和平均负载

linux 中进程的 D 状态全称是 Disk Sleep,在内核中用 TASK_UNINTERRUPTIBLE 来表示。任务阻塞态有两个,一个是 TASK_INTERRUPTIBLE,一个是 TASK_UNINTERRUPYIBLE。所谓阻塞态,一般是 io(网络 io 或者磁盘 io),比如网络收包的时候,如果当前没有数据,那么就会一直阻塞等待数据到来。TASK_INTERRUPTIBLE 是浅度睡眠,这种状态可以被信号唤醒,也可以被资源唤醒;TASK_UNINTERRUPTIBLE 是深度睡眠,处于深度睡眠时,任务只能被资源唤醒,不能被信号唤醒。当然,如果一个任务处于 D 状态,使用 kill 信号也是不能将之杀死的,因为 D 状态的进程不响应信号。

平均负载(load average),表示单位时间内,linux 运行队列中的任务个数。平均负载可以用 3 个命令来查看:top, uptime, w。这 3 个命令显示平均负载的 3 个值,从前向后分别是过去 1分钟,5分钟,15 分钟的平均负载。

top

 uptime

w

平均负载中的平均,并不是可运行任务数在 cpu 核上的平均,比如一台机器有 8 个 cpu 核,运行队列中等待的任务数是 16,那么平均负载就是 16/8 = 2,平均负载不是这样计算的。

平均负载是在时间维度上的平均,具体实现比较复杂,有兴趣可以参考 kernel/sched/loadavg.c。如果一台机器上有 8 个 cpu 核,平均负载是 4,那么说明 cpu 有 50% 的空闲;如果平均负载是 8,那么每个任务都有机会执行;如果平均负载是 12,那么说明机器的负载偏高了,一些应用可能会卡顿。

平均负载不仅仅统计了可以运行的任务,还统计了处于 D 状态的任务,下一节我们用一个内核模块来构造出 D 状态的任务,然后观察负载升高,以此来证明,D 状态的任务会增加系统负载。

2 负载升高示例

2.1 TASK_RUNNING

TASK_RUNNING 表示进程是可以运行的,可能正在运行也可能在运行队列中等待。

如下代码是一个 while(1) ,cpu 占用率接近 100%。会让平均负载升高 1。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main() {
    while (1) {
    }
    return 0;
}

2.2 TASK_UNINTERRUPTIBLE

使用一个内核模块来构造出 D 状态的线程,然后观察系统负载升高的过程。

对于此内核模块的介绍如下:

① 模块初始化函数,模块退出函数

一个内核模块需要使用 module_init() 和 module_exit() 声明这个模块的初始化函数和退出函数。这两个函数主要工作分别是资源的申请和资源的释放。使用这样的规范来约束开发者,可以尽量避免资源忘记释放的情况发生。像 c++ 中的类也有构造函数和析构函数,与这里的初始化函数和退出函数的作用是类似的。

module_init(disk_sleep_module_init);

module_exit(disk_sleep_module_exit);

② 这个内核模块注册了一个字符设备

模块加载之后,会在 /dev/ 下生成一个字符设备:disk_sleep_device。

字符设备的关键是声明了一个 struct file_operations 结构体,其中可以定义对该设备的读写函数,这也体现了 linux “一切皆文件” 的思想。

创建一个字符设备,需要依次调用 3 个函数来实现:register_chrdev(),class_create(), device_create()。注销一个字符设备也有对应的 3 个函数:device_destroy(),class_destroy(),unregister_chrdev()。
 

③ 通过在字符设备的读函数中设置 TASK_UNINTERRUPTIBLE 来构造 D 状态

模块加载之后通过 cat /dev/disk_sleep_device 可以触发调用模块的读函数 device_read(),在该函数中设置了 D 状态。

#include <asm/uaccess.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>

#define MODULE_NAME "disk_sleep_module"
#define DEVICE_NAME "disk_sleep_device"

static int major_number;
static char message[256] = {0};
static struct class *dev_class;
static struct device *device;

static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .read = device_read,
    .write = device_write,
    .release = device_release,
};

// 模块初始化函数
static int __init disk_sleep_module_init(void) {
  printk(KERN_INFO "disk_sleep_module: Initializing...
");
  
  // 注册字符串设备
  // 第 3 个参数是一个 struct file_operations 类型
  // 字符设备也可以当做一个文件来操作
  major_number = register_chrdev(0, DEVICE_NAME, &fops);
  if (major_number < 0) {
    printk("disk_sleep_module, failed to register char devm major number: %d
", major_number);
    return major_number;
  }
  printk("disk_sleep_module: registered correctly with major number %d
", major_number);
  
  // 创建设备 class
  dev_class = class_create(THIS_MODULE, DEVICE_NAME);
  if (IS_ERR(dev_class)) {
    printk("create dev class error.
");
    return -1;
  }
  
  // 创建设备
  device = device_create(dev_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
  if (IS_ERR(device)) {
    printk("create device error.
");
    return -1;
  }
  return 0;
}

// 模块退出函数
static void __exit disk_sleep_module_exit(void) {
  // 释放设备资源
  device_destroy(dev_class, MKDEV(major_number, 0));
  class_destroy(dev_class);
  // 注销设备
  unregister_chrdev(major_number, DEVICE_NAME);
  printk("disk_sleep_module: Goodbye from the LKM!
");
}

static int device_open(struct inode *inode, struct file *file) {
  printk("disk_sleep_module: Device has been opened
");
  return 0;
}

static int device_release(struct inode *inode, struct file *file) {
  printk("disk_sleep_module: Device successfully closed
");
  return 0;
}

static ssize_t device_read(struct file *file, char *buffer, size_t length, loff_t *offset) {
  int bytes_read = 0;

  printk("disk_sleep_module: before set uninterruptible
");
  __set_current_state(TASK_UNINTERRUPTIBLE);  // 改变进程状态为睡眠
  printk("disk_sleep_module: before schedule
");
  schedule();

  while (length && (message[*offset] != 0)) {
    put_user(message[*offset], buffer++);
    length--;
    bytes_read++;
    (*offset)++;
  }

  return bytes_read;
}

static ssize_t device_write(struct file *file, const char *buffer, size_t length, loff_t *offset) {
  int i;
  printk("device write: %s
", buffer);
  for (i = 0; i < length && i < sizeof(message); i++) {
    get_user(message[i], buffer + i);
  }
  return i;
}

module_init(disk_sleep_module_init);
module_exit(disk_sleep_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("a module to make D state");
MODULE_VERSION("0.1");

 Makefile

obj-m += disk_sleep.o

CONFIG_MODULE_SIG=n

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

执行 cat /dev/disk_sleep_device 之前,观察机器的平均负载在 0.20 左右。

执行 cat /dev/disk_sleep_device,如下图所示,执行了 3 次,构造了 3 个 D 状态的线程。

以线程 2845 为例,该线程的状态是 D 状态,disk sleep。

通过 top 查看,系统的负载在逐渐增大,最终会增长到 3 左右。

TASK_KILLABLE 状态的线程,同样也会显示为 D 状态,也会导致系统负载升高。

3 对 top 中 %wa 的理解

3.1 wa

使用 top 查看 cpu 使用率的时候,如果输入一个 1,那么就会显示每个 cpu 的使用情况。其中每个 cpu 使用率都详细统计到几项:us, sy, ni, id, wa, hi, si, st,其中 ni 和 st 使用比较少,这里不做记录。

us

用户态 cpu 使用率

sy

内核态线程 cpu 使用率

id

cpu 空闲

wa

wait,比如读写磁盘时的 wait

hi

硬中断

si

软中断

其中 wa 也称 io wait,当这个比例比较高时,说明达到了磁盘瓶颈。

使用命令 dd if=/dev/zero of=testfile bs=10M count=1000000 & 来模拟向磁盘写数据,我测试的时候起了 3 个。

从下图中可以看出来,dd 大部分时间处于 D 状态,也就是在访问磁盘的时候。cpu 占用率中的 wa 占比也比较高。load average 也在升高。

wa 并不是真正的占用 cpu,而表示在访问磁盘的时候,等待磁盘响应的时间。这段时间并不是真正的占用 cpu,cpu 是空闲的状态。也就是说如果有其它应用要占用很多 cpu,统计在 wa 里的 cpu 使用率会分配到这些应用上。

如下图所示,a.out 中就是一个 while(1),cpu 占用率将近 100%,起了 3 个这样的应用。a.out 启动之后,cpu 大部分统计到 us 中了,wa 的统计减少。

3.2 cpu 统计中的 wa 和内存统计中的 buff/cache 的相似之处

wa 接近于 id,其实 cpu 也是空闲的。这就类似于 free 命中显示的内存的使用情况,free 中的 buff/cache 表示文件读写时的 cache 所使用的内存,但是后边还有一个 available,available 大概意思就是 buff/cache 中可以回收的内存。buff/cache 的内存虽然在使用,但是大部分都是可以随时回收的,如果有应用需要大量的内存,那么可以将 buff/cache 中的内存回收进行使用。

linux 中有一个文件 /proc/sys/vm/drop_caches,向这个文件中写 1 会释放页缓存,写 2 会释放 dentry 和 inode 缓存,写 3 会释放页缓存以及 dentry 和 inode 缓存。如下图所示是操作过程,在操作前和操作后都用 free 查看内存使用情况,可以看到回收缓存之后,buff/cache 减小了,free 和 avalible 增大了。

free 命令显示了两行数据,第一行是物理内存的使用情况,第二行是交换分区的使用情况。交换分区一般是当系统内存比较紧张时,使用磁盘来当内存来使用。

free 命令显示的数字,默认情况下单位是 KB。

内存:

total

used

free

shared

buff/cache

available

物理内存总量

使用的内存

空闲内存

共享内存

页缓存,dentry, inode 缓存

可以使用的内存

 total = used + free + buff/cache

其中 free 表示空闲的内存,available 表示可以使用的内存,available 是考虑了 free 以及 buff/cache 中可以回收的内存之后统计出来的结果。

shared 是被多个进程共享的内存,是正在被使用,不能回收的内存。