linux 负载进程,Linux 中 D 状态的进程与平均负载

这篇文章聊聊 Linux 中 D 状态的进程与平均负载的关系,通过阅读本文,你会了解到这些东西。

D 状态的进程是什么

如何编写内核模块模拟 D 状态进程

Linus 对 D 状态进程的看法

平均负载的概念

在 top 和 uptime 命令输出中的第一行有一个 load average 字段,由三个数字表示,依次表示过去 1 分钟、5 分钟、15 分钟的平均负载(Load Average),如下图所示。

7d442993878a3eade2f2942b29c69594.png

值得注意的是,平均负载并不是指 CPU 的负载,这也比好理解,毕竟系统资源并不是只有 CPU 这一个。简单来看,平均负载是指单位时间内,系统处于 可运行状态 和 不可中断状态 的平均进程数,也就是平均活跃进程数。实际的计算比较复杂,感兴趣的同学可以查看源码 github.com/torvalds/li… 。

从直观的角度理解,如果平均负载为 2,在 4 核的机器上,表示有 50% 的 CPU 空闲;在 2 核的机器上,表示刚好没有 CPU 空闲,如果是单核的机器,那表明 CPU 竞争激烈,有一半的进程竞争不到 CPU。

进程运行的几种状态如下图所示。

6faa75988a37b23e0226f8e1b27fe6ba.png

当使用 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 也不例外。

“不可中断”指的是当前正处于内核中的关键流程,不可以被打断,比较常见的是读取磁盘文件的过程中被打断去处理信号,读到的内容就是不完整的。

从侧面来看,磁盘的驱动是工作在内核中的,如果磁盘出现了故障,磁盘读不到数据,内核就陷入了很尴尬的两难局面,这个锅只能自己扛着,将进程标记为不可中断,谁让磁盘驱动是跑在内核中呢。

之前有人给大神 Linus 发信希望移除 TASK_UNINTERRUPTIBLE 这个状态,Linus 在 kernel.org 邮件组中专门回答过为什么 D 状态的进程必不可少,链接如下 lore.kernel.org/lkml/Pine.L… ,我截了一个图放在了下面。

1a8747bca79a35b13689407b65aa62f8.png

如果只是这些问题,倒也平平无奇,不关我们什么事,但是需要注意的是 D 状态的进程会增加系统的平均负载。

下面我们来演示一下,如何通过编写一个系统内核模块,实现一个设备驱动文件,稳定复现展示 D 状态的进程,然后观察系统负载的变化。

内核模块编写

编写一个内核模块非常简单,新建一个 my_char_dev.c 文件,基本的框架如下所示。

int my_module_init(void) {

printk("my module loaded\n");

return 0;

}

void my_module_exit(void) {

printk("my module unloaded\n");

}

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

接下来实现画马的最后一步,给这个内核模块添加字符设备读取写入的逻辑

257f49dcee0573c9bc8b7ad8362ceaaa.png

也来添加一下其他的细节,代码如下所示。

#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 loaded\n");

// register_chrdev 函数的 major 参数如果等于 0,则表示采用系统动态分配的主设备号

major_num = register_chrdev(0, DEVICE_NAME, &fops);

if (major_num < 0) {

printk("Registering char device failed with %d\n", 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 unloaded\n");

}

这里首先在内核模块初始化回调中使用 register_chrdev 函数注册一个字符设备驱动,随后使用 class_create 和 device_create 函数创建 /dev/mychardev 设备文件。同时定义了这个设备文件的 open、release、read、write 处理函数。

static int my_device_open(struct inode *inode, struct file *file) {

printk("%s\n", __func__);

return 0;

}

static int my_device_release(struct inode *inode, struct file *file) {

printk("%s\n", __func__);

return 0;

}

static ssize_t my_device_read(struct file *file,

char *buffer, size_t length, loff_t *offset) {

printk("%s %u\n", __func__, length);

return 0;

}

static ssize_t my_device_write(struct file *file,

const char *buffer, size_t length, loff_t *offset) {

printk("%s %u\n", __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("%s\n", __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 %u\n", __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("%s\n", __func__);

// 使用一个静态的局部变量,记录设备文件打开的次数, 每次 cat,这个 counter 加一

static int counter = 0;

if (counter == 2) {

__set_current_state(TASK_UNINTERRUPTIBLE); //改变进程状态为睡眠

schedule();

}

p = msg;

++counter;

return 0;

}

再次编译加载这个文件,执行几次 cat,会发现在第 3 次的时候,cat 阻塞没有输出,如下所示。

71bcfef26dbaef102f888ab01e8699fb.png

使用 top 命令查看 cat 进程的状态。

4f222fe6baa50a692322575610b0d1f1.png

可以看到 cat 进程的状态为 D,CPU 占用为 0%,但是系统的 load average 在持续升高,运行一段时间会稳定到达 1,如下所示。

7ba4b5ee3d6706f0e169ba3f71cf4b8b.png

如果再启动两个 cat,那么 load average 会升高到 3,如下所示。

278761f52ae6a92337d5dcf56e78d99d.png

到这里我们就非常快速的模拟了 D 状态,以及观察了 D 状态对系统的 load average 的影响。希望能给你提供一些不一样的方式,加深你对平均负载的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值