Процессы и средние нагрузки в D-состоянии в Linux

Архитектура оптимизация производительности

В этой статье рассказывается о взаимосвязи между процессами состояния D и средней нагрузкой в ​​Linux.Прочитав эту статью, вы узнаете об этих вещах.

  • Что такое процесс в состоянии D?
  • Как написать модуль ядра для имитации процесса D-состояния
  • Взгляд Линуса на процессы D-состояния
  • Понятие средней нагрузки

В первой строке выходных данных команд top и uptime есть поле средней нагрузки, представленное тремя числами, которые, в свою очередь, представляют среднюю нагрузку за последние 1 минуту, 5 минут и 15 минут, как показано на следующем рисунке.

Стоит отметить, что средняя загрузка не относится к загрузке ЦП, что проще для понимания, ведь системный ресурс — это не только ЦП. Проще говоря, средняя нагрузка относится к единице времени, в течение которого система находится в可运行状态и不可中断状态Среднее количество процессов, то есть среднее количество активных процессов. Фактический расчет более сложен, и заинтересованные студенты могут просмотреть исходный код.GitHub.com/Tor val all/Li….

С интуитивной точки зрения, если средняя нагрузка равна 2, на 4-ядерной машине это означает, что 50% процессора простаивает, на 2-ядерной машине это означает, что простоя процессора нет, если это одноядерная машина, это означает, что конкуренция за ЦП жесткая: половина процессов борется за меньшее, чем ЦП.

Несколько состояний запущенного процесса показаны на следующем рисунке.

Когда системный вызов, такой как fork(), используется для создания нового процесса, состоянием нового процесса является состояние готовности.В Linux процесс в состоянии готовности также принадлежитTASK_RUNNINGСтатус, на данный момент право на использование ЦП еще не получено.

Процессы в состояниях «Готово» и «Выполняется» на рисунке — это все процессы в «состоянии выполнения», соответствующем отметке R в верхней команде.

Процесс в состоянии «Выполняется» переходит в состояние «Заблокировано» в ожидании какого-либо события или ресурса. Прерываемые процессы (TASK_INTERRUPTIBLE) могут быть разбужены с помощью сигналов и пробуждения и повторно войти в состояние готовности, соответствующее процессам, помеченным буквой S вверху. Что, черт возьми, такое непрерывное состояние (TASK_UNINTERRUPTIBLE)?

Процесс в состоянии D

TASK_UNINTERRUPTIBLE отображается в верхней команде как метка D, что является хорошо известным процессом «состояние D». Как следует из названия, процесс в состоянии TASK_UNINTERRUPTIBLE не может быть разбужен сигналом, только пробуждением. Так как TASK_UNINTERRUPTIBLE не может быть разбужен сигналом, то естественно не будет реагировать на команду kill, даже kill -9 не исключение.

"Непрерывный" относится к ключевому процессу, находящемуся в данный момент в ядре, который не может быть прерван. Чаще всего процесс чтения дискового файла прерывается для обработки сигнала, а прочитанное содержимое является неполным.

Со стороны привод диска работает в ядре.Если диск выйдет из строя и диск не сможет прочитать данные, ядро ​​попадет в очень неловкую дилемму.Этот горшок можно нести только сам по себе, и процесс будет быть помечен как неприемлемый.Прерывание, позволяющее диску работать в ядре.

Кто-то отправил письмо великому богу Линусу раньше, в надежде убрать состояние TASK_UNINTERRUPTIBLE. Линус конкретно ответил, зачем нужен процесс состояния D в почтовой группе kernel.org. Ссылка следующаяlore.kernel.org/command now/pine.l…, я сделал скриншот и поместил его ниже.

Если бы были только эти проблемы, это было бы тривиально и не наше дело, но следует отметить, что процесс в состоянии 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

Далее реализуем последний шаг рисования лошадей, добавляем в этот модуль ядра логику для чтения и записи символьных устройств

Также, чтобы добавить некоторые другие детали, код показан ниже.

#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. В то же время определяются функции обработки открытия, освобождения, чтения и записи этого файла устройства.

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

Затем мы вносим небольшие изменения, чтобы кошка могла выводить «привет, мир!», и измененный код выглядит следующим образом.

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!

Далее заходим в тему, устанавливаем статус TASK_UNINTERRUPTIBLE после того, как пользователь прочитает 2 раза, и модифицируем код 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 несколько раз, и вы обнаружите, что в третий раз cat блокируется без вывода, как показано ниже.

Используйте команду top для просмотра состояния процесса cat.

Видно, что статус процесса cat — D, а загрузка ЦП — 0%, но средняя загрузка системы продолжает расти, и она неуклонно достигает 1 после работы в течение определенного периода времени, как показано ниже. .

Если запустить еще двух кошек, средняя нагрузка увеличится до 3, как показано ниже.

До сих пор мы очень быстро моделировали состояние D и наблюдали влияние состояния D на среднюю нагрузку системы. Я надеюсь предоставить вам несколько различных способов углубить ваше понимание средней нагрузки.

считать

Вообще говоря, чтение и запись устройств ввода-вывода происходит относительно быстро.Если устройство ввода-вывода имеет узкое место, это неизбежно приведет к тому, что большое количество процессов будет ожидать ввода-вывода.В этом случае, хотя это не имеет никакого отношения к ЦП, вычислительная мощность всей системы на самом деле была большим узким местом, поэтому разумно учитывать процесс в состоянии D в средней нагрузке. Когда средняя загрузка системы относительно высока, сначала нам нужно определить, является ли это проблемой ЦП или проблемой ввода-вывода.

Адрес исходного кода проекта:GitHub.com/Артур-Станция…

Если у вас есть какие-либо вопросы, вы можете отсканировать QR-код ниже и подписаться на мою официальную учетную запись, чтобы связаться со мной.