僵尸态是 linux 进程的一种状态,用 Z (zombie) 表示。处于 Z 状态的进程已经不在工作,进程的资源(内存,打开的文件) 都已经释放,只保留 struct task_struct 一个空壳子,用僵尸来表示这个状态非常形象。僵尸进程不能被信号杀死(因为僵尸进程已经死了,当然也不能响应信号),只能被父进程回收。进程处于僵尸态时保存的信息非常少,其中包括进程号,退出码,退出码是比较重要的,父进程回收僵尸进程的时候可以根据退出码确定子进程的退出原因。

D 状态全称是 disk sleep,是不可中断睡眠态,常常用在访问磁盘的场景,当线程被设置为 TASK_UNINTERRUPTIBLE 之后便会显示为 D 状态,处在这种状态下的进程只能被资源唤醒,不能被信号唤醒,kill -9 也杀不掉。

Z 状态和 D 状态没有直接的关系,两者的相同点是不能响应信号;区别是 Z 状态的进程已经死了,D 状态的进程还在工作。

1 Z 状态

1.1 僵尸进程(Z 状态)是如何产生的

当子进程退出之后,如果父进程没有通过 wait() 进行回收,那么这个进程就会变成僵尸进程。父进程通过 wait() 可以获取子进程退出的状态,比如是被某个信号(SIGTERM, SIGABRT 等)杀死的或者退出码(0, -1 等)是什么。

如下代码,创建了一个子进程,子进程打印一条日志之后就退出了,父进程睡眠了 20s,在子进程退出,父进程睡眠的这 20s 内,子进程的状态是 Z 状态。

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

int main(void) {
  pid_t pid = fork();
  if (pid < 0) {
    perror("fork failed.");
    exit(1);
  }

  if (pid > 0) {
    printf("this is the parent process, pid is %d.\n", getpid());
    sleep(20);
  } else if (pid == 0) {
    printf("this is the child process. pid is: %d. ppid is: %d.\n", getpid(), getppid());
    return 0;
  }

  return 0;
}

将上边的代码编译,运行,可以看到父进程的进程号是 3247, 子进程的进程号是 3248。子进程退出之后处于 Z 状态,处于 Z 状态的进程,进程名用中括号括起来(僵尸放到了棺材里),并且后边标记为 defunct。

僵尸进程是不再工作的进程,是已经死掉的进程,不能通过 kill -9 杀掉。

要想让僵尸进程消失,可以杀掉它的父进程。父进程被杀掉之后,僵尸进程会变成孤儿进程,孤儿进程会被系统的 1 号进程收留(托孤),1 号进程会检查该进程是不是僵尸进程,如果是的话则会将进程回收。

孤儿进程会托孤给 1 号进程,下边的代码显示了这个过程,父进程 sleep 5s 之后退出,子进程 sleep 8s,也就是说在子进程退出之前父进程已经退出了,这样父进程不会回收子进程。子进程在 sleep 前后分别打印 ppid(parent pid 父进程的 pid), 可以看到 sleep 之后打印的父进程 id 是 1。

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

int main(void) {
  pid_t pid = fork();
  if (pid < 0) {
    perror("fork failed.");
    exit(1);
  }

  if (pid > 0) {
    printf("\nthis is the parent process, pid: %d\n", getpid());
    sleep(5);
    printf("\nparent process after sleep\n");
  } else if (pid == 0) {
    printf("\nthis is the child process. pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(8);
    printf("\nchild process after sleep, pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(100);
    return 0;
  }

  return 0;
}

1.2 假僵尸进程

一开始的时候说过,僵尸进程是不在工作的进程,资源都已经释放,并且通过 kill -9 杀不掉。本节记录的假的僵尸进程,通过 ps -aux 看到进程的状态是 Z,但是这个进程还在工作,资源没有释放,并且可以通过 kill -9 杀掉。

正常情况下主进程的退出是通过主线程中 return 或者调用 exit() 退出的,这种退出方式,整个进程都会退出。如果主线程通过 pthread_exit() 进行退出,那么退出的只是这个主线程,如果进程中有子线程的话,子线程不会退出,并且资源也不会释放。

下边这段代码,在主线程中创建了一个子线程,子线程中是 while(1) 循环。创建完子线程之后,主线程调用 pthread_exit() 退出,构造了一个主线程退出,但是子线程没有退出的场景。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void *thread_func(void *data) {
  printf("thread entry\n");
  while (1)
    ;
}

int main(void) {
  printf("pid: %d\n");

  pthread_t tid;
  pthread_create(&tid, NULL, thread_func, NULL);
  pthread_exit(NULL);

  return 0;
}

上述代码运行之后,通过 top 查看进程的情况,发现进程中有两个线程,分别是 3391 和 3392,其中 3391 是主线程,处于 Z 状态,3392 是子线程,处在运行态。主线程处于 Z 状态,所以查看进程的状态也是处于 Z 状态。这种状态是假的僵尸态,父进程也不会回收假的僵尸进程。

1.3 父进程是 1 号进程的僵尸进程是怎么产生的

从上边的分析来看,如果进程先变成了僵尸进程,然后托孤给 1 号进程,那么托孤给 1 号进程之后,1 号进程就会回收这个僵尸进程;如果进程先变成孤儿进程,那么变为孤儿进程时,便会托孤给 1 号进程,之后进程状态转换为僵尸进程时,也会被 1 号进程回收。上边这两种情况下,如果僵尸进程的父进程是 1 号进程,那么僵尸进程很快就会被 1 号进程回收,僵尸进程存在的时间比较短。

另外还有一种情况,僵尸进程的父进程是 1 号进程的状态存在的时间比较长,僵尸进程并没有被回收。以下 3 中条件会出现这种情况:

(1)僵尸进程是假僵尸进程

(2)进程被使用,比如被 gdb 调试

(3)进程中有子线程处于 D 状态

下边分别用例子来构造出上边 3 种情况:

(1)僵尸进程是假僵尸进程

如下代码,fork 出一个子进程,之后父进程退出,父进程退出之后,子进程被过继给进程 1。子进程中创建了一个子线程之后,主线程通过 pthread_exit() 退出,所以子进程就成了假僵尸进程,假僵尸进程可以直接 kill 掉。这个时候假的僵尸进程的父进程是 1 号进程,并且这个进程还没有真正退出。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void *thread_func(void *data) {
  printf("thread entry\n");
  while (1)
    ;
}

int main(void) {
  printf("pid: %d\n");

  if (fork() == 0) {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_exit(NULL);
  }

  return 0;
}

(2)进程被使用,比如被 gdb 调试

如下的代码,fork() 一个子进程之后,父子进程都执行 while(1) 循环,如果使用 gdb --pid 子进程 id 之后,然后再 kill 掉父进程和子进程之后,父进程会直接退出,子进程会成为父进程是 1 号进程的僵尸进程,这种进程待 gdb 退出之后才会被回收,还在被 gdb 跟踪的过程中,1 号进程不会回收这个僵尸进程。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
  int i = 0;

  fork();
  while (1) {
    printf("i: %d\n", i++);
    sleep(1);
  }
  return 0;
}

(3)进程中有子线程处于 D 状态

如果主线程已经退出,但是子线程中有 D 状态,这个时候跟假的僵尸进程是比较类似的。

2 D 状态

2.1 D 状态 TASK_UNINTERRUPTIBLE

为了模拟出 TASK_UNINTERRUPTIBLE 的状态,我们通过内核模块来实现。

下边这个内核模块中有一个 cb 参数,该参数可以通过命令行 echo xxx > xxx 进行修改,在修改这个参数的时候,notify_param 便会被调用,在该函数中将线程设置为 TASK_UNINTERRUPTIBLE 然后调用 schedule() 让出 cpu,这种情况下,echo 命令便会卡住。在设置状态之前打印了当前线程的 id,这样可以定位到是哪个线程。

之所以通过 cb 参数的方式来模拟出 D 状态,而不直接创建内核线程,再设置内核线程为 D 状态,是因为前者的参数设置函数可以在用户态调用到,也就是最终设置为 D 状态的线程是一个用户态的线程,而如果在内核线程里边设置 D 状态,那么这个线程只是个内核态的线程。而 kill 信号只能发送给用户态的进程,对内核态线程无效, 后边要说明处于 TASK_UNINTERRUPTIBLE 状态的进程,使用 kill 命令杀不掉,所以使用了 cb 参数的方式来模拟出 D 状态。

内核模块源码:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

int cb_value = 0;

int notify_param(const char *val, const struct kernel_param *kp)
{
    int res = param_set_int(val, kp);
    printk("pid %d\n", current->pid);
    set_current_state(TASK_UNINTERRUPTIBLE);
    schedule();
    if (res == 0)
    {
        printk(KERN_INFO "call back function called, new value: %d\n", cb_value);
        return 0;
    }
    return -1;
}

const struct kernel_param_ops test_param_ops =
{
    .set = &notify_param,
    .get = &param_get_int,
};

module_param_cb(cb_value, &test_param_ops, &cb_value, S_IRUGO | S_IWUSR);

static int __init test_init(void)
{
    printk(KERN_INFO "cb_value = %d  \n", cb_value);
    printk(KERN_INFO "test module is installed\n");
    return 0;
}

static void __exit test_exit(void)
{
    printk(KERN_INFO "test module is removed\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("w2x");
MODULE_DESCRIPTION("test disk sleep by module_param_cb");
MODULE_VERSION("1.0");

内核模块编译脚本:

obj-m += test.o
KDIR =/lib/modules/$(shell uname -r)/build
all:
        make -C $(KDIR) M=$(shell pwd) modules
clean:
        make -C $(KDIR) M=$(shell pwd) clean

 操作步骤:

(1)编译之后安装模块,通过如下命令设置 cb_value 的值,那么进程就会进入 D 状态。

echo 10 > /sys/module/test/parameters/cb_value

(2)通过 dmesg 查看到线程号是 2402

(3)通过 ps -aux |grep 2402 查看到该线程处于 D 状态

这样我们就构造除了一个处于 D 状态的用户态进程。

 (4)通过 cat /proc/2402/stack 看到线程的调用栈,该线程调用到了 notify_param

(5)2402 通过 kill -9 杀不掉

设置 TASK_UNINTERRUPTIBEL 之后,线程处于 D 状态,D 状态又叫 disk sleep,与访问磁盘有关,该状态通过 kill 信号杀不掉,只能等到条件满足之后才可从 D 状态中返回。

D 状态也不仅仅用在磁盘操作的场景,当内核出现异常的时候,有时也会将线程设置为 D 状态,如下代码所示,当进程在退出的时候,内核出现了 bug,会递归调用退出函数,这个时候内核就会停止该进程的退出过程而把它设置为 D 状态。本文后边讲的问题就是这个现象。

void __noreturn do_exit(long code)
{
    /*
     * We're taking recursive faults here in do_exit. Safest is to just
     * leave this task alone and wait for reboot.
     */
    if (unlikely(tsk->flags & PF_EXITING)) {
        pr_alert("Fixing recursive fault but reboot is needed!\n");
        futex_exit_recursive(tsk);
        set_current_state(TASK_UNINTERRUPTIBLE);
        schedule();
    }
}

另外,内核中的互斥体 mutex,如果不能立即锁,在等待锁的过程中,也会进入 D 状态。

如下内核模块,创建了一个内核线程,在内核线程中连续调用两次 mutex_lock(),第二次调用会造成死锁,该线程就会设置成 TASK_UNINTERRUPTIBLE 状态。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/kthread.h>
#include <linux/delay.h>

struct mutex test_mutex;

static int thread_func(void *data) {
    printk("test_thread_pid: %d\n", current->pid);
    mutex_init(&test_mutex);
    printk("1\n");
    mutex_lock(&test_mutex);
    printk("2\n");
    mutex_lock(&test_mutex);
    printk("3\n");
    return 0;
}

static int __init hello_init(void){
    printk("Hello World enteri, tid: %d\n", current->pid);
    kthread_run(thread_func, NULL, "test_thread");
    return 0;
}

static void __exit hello_exit(void){
    printk("Hello World exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("wx2");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("hello world kernel module");
MODULE_ALIAS("hello world kernel module");

如下是 mutex_lock 相关的代码,最终会调用 __mutex_lock,该函数的第二个入参是 TASK_UNINTERRUPTIBLE,在 __mutex_lock 中最终会将线程设置成不可中断睡眠状态。

static noinline void __sched
__mutex_lock_slowpath(struct mutex *lock)
{
    __mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_);
}

2.2 D 状态 TASK_KILLABLE

set_current_state(TASK_UNINTERRUPTIBLE);

将上一节(1.4 节) 中的 set_current_state(TASK_UNINTERRUPTIBLE) 中 TASK_UNINTERRUPTIBLE 改成 TASK_KILLABLE,进程仍然显示为 D 状态。

两者的区别是,TASK_KILLABLE 可以使用 kill -9 杀掉,处于TASK_UNINTERRUPTIBLE 状态的线程不能被杀掉。

2.3 hung task 检测

内核 hung task 检测功能可以检测出处于 TASK_UNINTERRUPTIBLE 的进程,有以下参数可以设置。

参数

含义

kernel.hung_task_timeout_secs = 120

线程处在 TASK_UNINTERRUPTIBLE 的超时时间,超时之后打印线程栈或者内核 panic

kernel.hung_task_panic = 0

如果设置为 1,那么超时之后内核 panic, 否则超时之后只打印线程栈,可以在 dmesg 中看到

kernel.hung_task_check_interval_secs = 0

检测周期

kernel.hung_task_warnings = 10

打印告警日志的次数

kernel.hung_task_check_count = 4194304

hung task 功能一次检测的线程的个数

kernel.hung_task_all_cpu_backtrace = 0

打印每个核的调用栈

告警信息:

[ 242.809728] INFO: task bash:2413 blocked for more than 120 seconds.

[ 242.809755] Tainted: G OE 5.15.0-67-generic #74~20.04.1-Ubuntu

[ 242.809757] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.

[ 242.809758] task:bash state:D stack: 0 pid: 2413 ppid: 2245 flags:0x00000000

[ 242.809761] Call Trace:

[ 242.809763]

[ 242.809766] __schedule+0x2cd/0x890

[ 242.809777] schedule+0x69/0x110

[ 242.809782] notify_param+0x3c/0x57 [test]

[ 242.809784] param_attr_store+0xa0/0x100

[ 242.809815] module_attr_store+0x20/0x40

[ 242.809821] sysfs_kf_write+0x3e/0x50

[ 242.809824] kernfs_fop_write_iter+0x13c/0x1d0

[ 242.809826] new_sync_write+0x117/0x1b0

[ 242.809829] vfs_write+0x189/0x270

[ 242.809830] ksys_write+0x67/0xf0

[ 242.809832] __x64_sys_write+0x1a/0x20

[ 242.809834] do_syscall_64+0x5c/0xc0

[ 242.809836] ? handle_mm_fault+0xd8/0x2c0

[ 242.809839] ? exit_to_user_mode_prepare+0x3d/0x1c0

[ 242.809844] ? do_user_addr_fault+0x1e0/0x660

[ 242.809847] ? irqentry_exit_to_user_mode+0x9/0x20

[ 242.809848] ? irqentry_exit+0x1d/0x30

[ 242.809849] ? exc_page_fault+0x89/0x170

[ 242.809852] entry_SYSCALL_64_after_hwframe+0x61/0xcb

[ 242.809854] RIP: 0033:0x7fe349068077

[ 242.809856] RSP: 002b:00007ffef8d504b8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001

[ 242.809858] RAX: ffffffffffffffda RBX: 0000000000000002 RCX: 00007fe349068077

[ 242.809860] RDX: 0000000000000002 RSI: 000055568ea38090 RDI: 0000000000000001

[ 242.809860] RBP: 000055568ea38090 R08: 000000000000000a R09: 0000000000000001

[ 242.809861] R10: 000055568c9d5017 R11: 0000000000000246 R12: 0000000000000002

[ 242.809862] R13: 00007fe3491476a0 R14: 00007fe3491434a0 R15: 00007fe3491428a0

hung task 检测的入口函数是 hung_task.c 中的 watchdog():

./linux-5.15.50/linux-5.15.50/kernel/hung_task.c

/*
 * kthread which checks for tasks stuck in D state
 */
static int watchdog(void *dummy)
{
    unsigned long hung_last_checked = jiffies;

    set_user_nice(current, 0);

    for ( ; ; ) {
        unsigned long timeout = sysctl_hung_task_timeout_secs;
        unsigned long interval = sysctl_hung_task_check_interval_secs;
        long t;

        if (interval == 0)
            interval = timeout;
        interval = min_t(unsigned long, interval, timeout);
        t = hung_timeout_jiffies(hung_last_checked, interval);
        if (t <= 0) {
            if (!atomic_xchg(&reset_hung_task, 0) &&
                !hung_detector_suspended)
                check_hung_uninterruptible_tasks(timeout);
            hung_last_checked = jiffies;
            continue;
        }
        schedule_timeout_interruptible(t);
    }

    return 0;
}

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

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

2.4.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.4.2 负载升高示例

2.4.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.4.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...\n");
  
  // 注册字符串设备
  // 第 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\n", major_number);
    return major_number;
  }
  printk("disk_sleep_module: registered correctly with major number %d\n", major_number);
  
  // 创建设备 class
  dev_class = class_create(THIS_MODULE, DEVICE_NAME);
  if (IS_ERR(dev_class)) {
    printk("create dev class error.\n");
    return -1;
  }
  
  // 创建设备
  device = device_create(dev_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
  if (IS_ERR(device)) {
    printk("create device error.\n");
    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!\n");
}

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

static int device_release(struct inode *inode, struct file *file) {
  printk("disk_sleep_module: Device successfully closed\n");
  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\n");
  __set_current_state(TASK_UNINTERRUPTIBLE);  // 改变进程状态为睡眠
  printk("disk_sleep_module: before schedule\n");
  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\n", 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 状态,也会导致系统负载升高。

2.4.3 对 top 中 %wa 的理解

2.4.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 的统计减少。

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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐