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 是被多个进程共享的内存,是正在被使用,不能回收的内存。