这篇文章说说Linux中D状态的进程与平均负载的关系,通过阅读本文,你会了解到这种东西。
在top和uptime命令输出中的第一行有一个loadaverage数组,由三个数字表示,依次表示过去1分钟、5分钟、15分钟的平均负载(LoadAverage),如右图所示。
值得注意的是,平均负载并不是指CPU的负载,这也比好理解,虽然系统资源并不是只有CPU这一个。简单来看,平均负载是指单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数。实际的估算比较复杂,感兴趣的朋友可以查看源码/torvalds/li…。
从直观的角度理解,若果平均负载为2,在4核的机器上,表示有50%的CPU空闲;在2核的机器上,表示正好没有CPU空闲,若果是单核的机器,那表明CPU竞争激烈,有一半的进程竞争不到CPU。
进程运行的几种状态如右图所示。
当使用fork()等系统调用来创建一个新进程时,新进程的状态是Ready状态,在linux中,就绪态的进程也属于TASK_RUNNING状态,这个时侯只是还没有领到CPU的使用权。
图中Ready和Running状态的进程都属于「可运行状态」的进程,对应top命令中R标记。
处于Running状态的进程在等待个别风波或资源时会步入Blocked状态。可中断的进程(TASK_INTERRUPTIBLE)可以被讯号和wakeup唤起,重新步入Ready就绪状态,对应于top中标记为S的进程。那不可中断(TASK_UNINTERRUPTIBLE)状态究竟是个哪些鬼?
D状态的进程
TASK_UNINTERRUPTIBLE在top命令中显示为D标记,也就是大名鼎鼎的「D状态」进程。顾名思义,处于TASK_UNINTERRUPTIBLE状态的进程不能被讯号唤起,只能由wakeup唤起。既然TASK_UNINTERRUPTIBLE不能被讯号唤起,自然也不会响应kill命令,即使是必杀kill-9也不例外。
“不可中断”指的是当前正处于内核中的关键流程linux 系统平均负载,不可以被打断,比较常见的是读取c盘文件的过程中被打断去处理讯号,读到的内容就是不完整的。
从侧面来看,c盘的驱动是工作在内核中的,假如c盘出现了故障,c盘读不到数据,内核就深陷了很难堪的两难局面,这个锅只能自己扛着,将进程标记为不可中断,谁让c盘驱动是跑在内核中呢。
之前有人给前辈Linus发信希望移除TASK_UNINTERRUPTIBLE这个状态,Linus在电邮组中专门回答过为何D状态的进程必不可少,链接如下/lkml/Pine.L…,我截了一个图置于了下边。
假如只是那些问题,倒也平平无奇,不关我们哪些事,并且须要注意的是D状态的进程会降低系统的平均负载。
下边我们来演示一下,怎样通过编撰一个系统内核模块,实现一个设备驱动文件,稳定复现展示D状态的进程,之后观察系统负载的变化。
内核模块编撰
编撰一个内核模块特别简单,新建一个my_char_dev.c文件,基本的框架如下所示。
int my_module_init(void) {
printk("my module loadedn");
return 0;
}
void my_module_exit(void) {
printk("my module unloadedn");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Arthur.Zhang");
MODULE_DESCRIPTION("A simple char device driver");
module_init和module_exit拿来定义内核模块的加载和卸载函数入口。printk用于复印内核复印,使用dmesg可以查看输出的信息。
之后编撰一个Makefile文件,如下所示。
obj-m += my_char_dev.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
insmod:
sudo insmod my_char_dev.ko
rmmod:
sudo rmmod my_char_dev.ko
执行make编译里面的文件,会生成一个my_char_dev.ko文件,此后使用insmod加载这个内核模块
sudo insmod my_char_dev.ko
之后使用dmesg-T就可以看见调用了module_init反弹函数,复印了内核模块加载成功句子。
[Wed Apr 22 02:52:07 2020] my module loaded
使用rmmod可以卸载这个模块。
sudo rmmod my_char_dev.ko
同样使用dmesg-T,可以看见调用了module_exit反弹函数。
[Wed Apr 22 02:54:46 2020] my module unloaded
接出来实现画马的最后一步linux系统下载官网,给这个内核模块添加字符设备读取写入的逻辑
也来添加一下其他的细节linux 系统平均负载linux培训班,代码如下所示。
#define DEVICE_NAME "mychardev"
int major_num;
struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_device_open,
.release = my_device_release,
.read = my_device_read,
.write = my_device_write,
};
/**
* 内核模块初始化
*/
int my_module_init(void) {
printk("my module loadedn");
// register_chrdev 函数的 major 参数如果等于 0,则表示采用系统动态分配的主设备号
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) {
printk("Registering char device failed with %dn", major_num);
return major_num;
}
// 接下来使用 class_create 和 device_create 自动创建 /dev/mychardev 设备文件
my_class_class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(my_class_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
return 0;
}
/**
* 内核模块卸载
*/
void my_module_exit(void) {
device_destroy(my_class_class, MKDEV(major_num, 0));
class_destroy(my_class_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk("my module unloadedn");
}
这儿首先在内核模块初始化反弹中使用register_chrdev函数注册一个字符设备驱动,此后使用class_create和device_create函数创建/dev/mychardev设备文件。同时定义了这个设备文件的open、release、read、write处理函数。
static int my_device_open(struct inode *inode, struct file *file) {
printk("%sn", __func__);
return 0;
}
static int my_device_release(struct inode *inode, struct file *file) {
printk("%sn", __func__);
return 0;
}
static ssize_t my_device_read(struct file *file,
char *buffer, size_t length, loff_t *offset) {
printk("%s %un", __func__, length);
return 0;
}
static ssize_t my_device_write(struct file *file,
const char *buffer, size_t length, loff_t *offset) {
printk("%s %un", __func__, length);
return length;
}
再现编译生成新的ko文件,加载运行,会生成一个/dev/mychardev设备驱动文件,如下所示。
$ ls -l /dev/mychardev
crw-------. 1 root root 245, 0 Apr 22 20:07 /dev/mychardev
接出来可以使用cat和echo对这个设备文件进行读写。
sudo cat /dev/mychardev
dmesg 输出
[Wed Apr 22 02:07:31 2020] my_device_open
[Wed Apr 22 02:07:31 2020] my_device_read 65536
[Wed Apr 22 02:07:31 2020] my_device_release
sudo sh -c "echo hello > /dev/mychardev"
[Wed Apr 22 02:09:20 2020] my_device_open
[Wed Apr 22 02:09:20 2020] my_device_write 6
[Wed Apr 22 02:09:20 2020] my_device_release
接下,我们做细微的调整,让cat输出"hello,world!",更改代码如下所示。
static char msg[] = "hello, world!n";
char *p;
/**
* 设备文件打开的回调
*/
static int my_device_open(struct inode *inode, struct file *file) {
printk("%sn", __func__);
p = msg;
return 0;
}
/**
* 处理 cat 等读取该设备文件的逻辑,返回 "hello, world!" 字符串到用户终端输出
*/
static ssize_t my_device_read(struct file *file,
char *buffer, size_t length, loff_t *offset) {
printk("%s %un", __func__, length);
int bytes_read = 0;
if (*p == 0) return 0;
while (length && *p) {
put_user(*(p++), buffer++);
length--;
bytes_read++;
}
return bytes_read;
}
这时,使用cat就可以在终端中见到输出的"hello,world!"字符串了,如下所示。
$ sudo cat /dev/mychardev
hello, world!
接出来我们来步入主题,在用户读取2次之后将状态设置为TASK_UNINTERRUPTIBLE,更改my_device_open的代码
static int my_device_open(struct inode *inode, struct file *file) {
printk("%sn", __func__);
// 使用一个静态的局部变量,记录设备文件打开的次数, 每次 cat,这个 counter 加一
static int counter = 0;
if (counter == 2) {
__set_current_state(TASK_UNINTERRUPTIBLE); //改变进程状态为睡眠
schedule();
}
p = msg;
++counter;
return 0;
}
再度编译加载这个文件,执行几次cat,会发觉在第3次的时侯,cat阻塞没有输出,如下所示。
使用top命令查看cat进程的状态。
可以看见cat进程的状态为D,CPU占用为0%,并且系统的loadaverage在持续下降,运行一段时间会稳定抵达1,如下所示。
假如再启动两个cat,这么loadaverage会下降到3,如下所示。
到这儿我们就十分快速的模拟了D状态,以及观察了D状态对系统的loadaverage的影响。希望能给你提供一些不一样的方法,加深你对平均负载的理解。
思索
通常来说,IO设备的读写是比较快的,假如IO设备出现困局,势必会引起大量的进程处于等待IO的状态,这些情况下,即使不关CPU哪些事,整个系统的处理能力虽然早已出现的很大的困局,所以把D状态的进程算在平均负载里也还算合理。当系统loadaverage比较高时,首先我们须要去甄别,究竟是CPU的问题还是IO的问题。
项目源码地址:/arthur-zhan…