Небольшой вопрос о том, что /dev/null почти вживую ест обувь

Java исходный код

Будут использоваться наши временные задачи, асинхронные программы пакетов MQ jar и т. д.System.in.read()В ожидании, пока блокирующая программа предотвратит выход программы, в локальном тестировании не возникало никаких проблем до получения отзывов от студентов, код System.in.read() в онлайн-среде Docker не блокировался, а последующая программа выполнялась. Упрощенный код выглядит следующим образом.

public static void main(String[] args) throws IOException, InterruptedException {
    System.out.println("enter main....");
    // 启动定时任务
    startJobSchedule();
    
    System.out.println("before system in read....");
    System.in.read();
    System.out.println("after system in read....");
}

Я взглянул и подумал, что это невозможно, код точно блокировался бы наSystem.in.read(), а потом сказать, что если будет выведено "после того, как system in read....", я буду есть туфли вживую. Результат действительно попыткаSystem.in.read();Вышел, выполнил последующее заявление, и туфли были немедленно поданы, ммм, действительно ароматные.

Прочитав эту статью, вы освоите следующие знания.

  • Связь между процессом и файловым дескриптором fd
  • Входы и выходы файла /dev/null, чтение и запись анализа исходного кода ядра
  • Характер перенаправления
  • Предварительное исследование концепции трубопровода

Дескриптор процесса и файла fd

Далее давайте посмотрим на связь между процессом и файловым дескриптором fd. После запуска процесса, в дополнение к выделению пространства в куче и стеке, он также по умолчанию выделяет три дескриптора файловых дескрипторов: стандартный ввод № 0 (stdin), стандартный вывод № 1 (stdout), вывод ошибок № 2 (stderr). ), как показано ниже.

Далее я проанализировал случай в начале: System.in.read() на самом деле считывает данные со стандартного ввода с fd равным 0. Мы будемSystem.in.read()Возвращаемое значение и прочитанное содержимое выводятся на печать.После экспериментов возвращаемое значение равно -1, и считывается EOF. Это довольно странно, почему чтение стандартного ввода возвращает EOF?

Затем посмотрите, на что указывает стандартный ввод с fd равным 0. Все дескрипторы открытых файлов процесса хранятся в каталоге /proc/pid/fd системы.Используйте ls для просмотра списка открытых в данный момент дескрипторов, как показано ниже.

$ ls -l /proc/1/fd
total 0
lrwx------ 1 root root 64 4月   3 17:13 0 -> /dev/null
l-wx------ 1 root root 64 4月   3 17:13 1 -> pipe:[31508]
l-wx------ 1 root root 64 4月   3 17:13 2 -> pipe:[31509]
l-wx------ 1 root root 64 4月   3 17:13 3 -> /app/logs/gc.log
lr-x------ 1 root root 64 4月   3 17:13 4 -> /jdk8/jre/lib/rt.jar
lr-x------ 1 root root 64 4月   3 17:13 5 -> /app/system-in-read-1.0-SNAPSHOT.jar

Вы можете видеть, что fd 0 указывает на/dev/null. см. далее/dev/nullсоответствующие знания.

/dev/нулевой файл

что за файл /dev/null

/dev/nullявляется специальным файлом устройства, и все полученные данные отбрасываются. кто-то положил/dev/nullЕго уместнее сравнить с «черной дырой».

В дополнение к функции отбрасывания всех записей, из/dev/nullЧтение данных немедленно вернет EOF, поэтому предыдущий вызов System.in.read() завершается напрямую.

Используйте stat для просмотра /dev/null, и результат будет следующим.

$ stat /dev/null
  File: ‘/dev/null’
  Size: 0         	Blocks: 0          IO Block: 4096   character special file
Device: 5h/5d	Inode: 6069        Links: 1     Device type: 1,3
Access: (0666/crw-rw-rw-)  Uid: (    0/    root)   Gid: (    0/    root)
Context: system_u:object_r:null_device_t:s0
Access: 2020-03-27 19:27:37.857000000 +0800
Modify: 2020-03-27 19:27:37.857000000 +0800
Change: 2020-03-27 19:27:37.857000000 +0800

$ who -b
         system boot  2020-03-27 19:27

Видно, что размер файла /dev/null равен 0, а время создания и модификации соответствует времени запуска системы ядра. Это не файл на диске, а файл типа «файл символьного устройства», который существует в памяти.

Все данные, записанные в этот файл, будут отброшены, вызов записи всегда будет возвращать успех, этот конкретный файл не будет заполнен, и размер его файла не может быть изменен.

Еще одно интересное явление заключается в том, что использование tail -f /dev/null приведет к блокировке навсегда.Вывод команды strace упрощен, как показано ниже.

$ strace tail -f /dev/null

open("/dev/null", O_RDONLY)             = 3
read(3, "", 8192)                       = 0
inotify_init()                          = 4
inotify_add_watch(4, "/dev/null", IN_MODIFY|IN_ATTRIB|IN_DELETE_SELF|IN_MOVE_SELF) = 1
read(4,

Можно видеть, что вызов чтения tail -f, который считывает /dev/null во время выполнения, возвращает 0, указывая на то, что он столкнулся с EOF, а затем tail использует системный вызов inotify_init для создания экземпляра inotify, который прослушивает /dev/IN_MODIFY. , события IN_ATTRIB, IN_DELETE_SELF, IN_DELETE_SELF для пустых файлов. Смысл этих четырех событий следующий.

  • IN_MODIFY: файл был изменен
  • IN_ATTRIB: изменение метаданных файла
  • IN_DELETE_SELF: слушать каталоги/файлы, которые нужно удалить
  • IN_MOVE_SELF: слушать перемещаемые каталоги/файлы

Затем заблокируйте ожидание возникновения этих событий, потому что эти события не происходят в /dev/null, поэтому команда tail будет заблокирована навсегда.

/dev/null с точки зрения источника

Логика ядра для обработки /dev/null находится вGitHub.com/Tor val all/Li…, код для записи данных в /dev/null находится в функции write_null.Исходный код этой функции показан ниже.

static ssize_t write_null(struct file *file, const char __user *buf,
			  size_t count, loff_t *ppos)
{
	return count;
}

Вы можете видеть, что при записи данных в /dev/null ядро ​​ничего не делает, кроме как возвращает входящее значение счетчика.

Код чтения находится в функции read_null, и логика этой функции следующая.

static ssize_t read_null(struct file *file, char __user *buf,
			 size_t count, loff_t *ppos)
{
	return 0;
}

Как видите, чтение /dev/null немедленно возвращает 0, что указывает на конец файла.

На данный момент здесь представлены знания, связанные с /dev/null. Почему нет проблем с родным тестом? Поскольку собственный тест использует терминальный терминал для запуска пакета jar, стандартный ввод процесса будет выделен для ввода с клавиатуры, и он всегда будет блокироваться, если не будут введены никакие символы. Далее давайте посмотрим, как воспроизвести эту проблему локально.

Файловые дескрипторы и перенаправление

Позиции стандартного ввода, стандартного вывода и вывода ошибок, описанные ранее в дескрипторе, не изменятся, но их направление можно изменить.Используемый нами оператор перенаправления>и<Он используется для перенаправления потока данных. Чтобы изменить стандартный ввод вышеуказанного процесса на/dev/null, просто используйте<характер перенаправления. Измените предыдущий код и добавьте спящий режим, чтобы предотвратить его выход.

public static void main(String[] args) throws IOException, InterruptedException {
    System.out.println("enter main....");
    byte[] buf = new byte[16];
    System.out.println("before system in read....");
    int length = System.in.read();
    System.out.println("len: " + length + "\t" + new String(buf));
    TimeUnit.DAYS.sleep(1);
}

Упакуйте и запустите, вывод выглядит следующим образом.

$ java -jar system-in-read-1.0-SNAPSHOT.jar < /dev/null

enter main....
before system in read....
len: -1

Видно, что происходит то же явление, что и в онлайн-среде докеров: System.in.read() не блокируется и возвращает -1.

Просмотрите список fd процесса следующим образом:

$ ls -l  /proc/482/fd

lr-x------. 1 ya ya 64 4月   3 20:00 0 -> /dev/null
lrwx------. 1 ya ya 64 4月   3 20:00 1 -> /dev/pts/6
lrwx------. 1 ya ya 64 4月   3 20:00 2 -> /dev/pts/6
lr-x------. 1 ya ya 64 4月   3 20:00 3 -> /usr/local/jdk/jre/lib/rt.jar
lr-x------. 1 ya ya 64 4月   3 20:00 4 -> /home/ya/system-in-read-1.0-SNAPSHOT.jar

Вы можете видеть, что стандартный ввод в это время был заменен на/dev/null, когда System.in.read() вызывается для чтения стандартного ввода, он сначала проверяет список файловых дескрипторов, чтобы увидеть, на какой поток данных указывает дескриптор 0, а затем считывает данные из этого потока данных.

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

  • 1>или>перенаправить стандартный вывод
  • 2>перенаправить стандартный вывод ошибок

Или можно использовать в комбинации:

java -jar system-in-read-1.0-SNAPSHOT.jar </dev/null > stdout.out 2> stderr.out

$ ls -l /proc/2629/fd

lr-x------. 1 ya ya 64 4月   3 20:35 0 -> /dev/null
l-wx------. 1 ya ya 64 4月   3 20:35 1 -> /home/ya/stdout.out
l-wx------. 1 ya ya 64 4月   3 20:35 2 -> /home/ya/stderr.out

Вы можете видеть, что файловые дескрипторы с fd 0, 1 и 2 на этот раз были заменены.

Что означает 2>&1, часто встречающееся в сценариях оболочки?

Разобранный,2>Это означает перенаправление stderr, а & 1 означает stdout.Смысл подключения заключается в перезаписи стандартного вывода ошибок stderr на тот же метод вывода, что и стандартный вывод stdout. Например, перенаправление стандартного вывода и стандартного вывода ошибок в файл можно записать следующим образом.

cat foo.txt > output.txt 2>&1

Далее продолжайте рассматривать концепции, связанные с файловыми дескрипторами и конвейерами.

трубопровод

Канал — это односторонний поток данных. Мы часто используем канал для соединения двух команд в командной строке. Возьмем в качестве примера следующую команду.

nc -l 9090 | grep "hello" | wc -l

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

  • процесс zsh, созданный из командной строки
  • процесс zsh запущен nc -l 9090 процесс
  • Процесс zsh запускает процесс grep и соединяет стандартный вывод процесса nc со стандартным вводом процесса grep через канал.
  • Процесс zsh запускает процесс wc и соединяет стандартный вывод процесса grep со стандартным вводом процесса wc через канал.

Их взаимосвязь процесса показана ниже.

  PID TTY      STAT   TIME COMMAND
23714 ?        Ss     0:00  \_ sshd: ya [priv]
23717 ?        S      0:00  |   \_ sshd: ya@pts/5  
23718 pts/5    Ss     0:00  |       \_ -zsh
 4812 pts/5    S+     0:00  |           \_ nc -l 9090
 4813 pts/5    S+     0:00  |           \_ grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exc
 4814 pts/5    S+     0:00  |           \_ wc -l

Просмотрите список файловых дескрипторов для процессов nc и grep следующим образом.


$ ls -l /proc/pid_of_nc/fd                                                                     

lrwx------. 1 ya ya 64 4月   3 21:22 0 -> /dev/pts/5
l-wx------. 1 ya ya 64 4月   3 21:22 1 -> pipe:[3852257]
lrwx------. 1 ya ya 64 4月   3 21:17 2 -> /dev/pts/5


$ ls -l /proc/pid_of_grep/fd

lr-x------. 1 ya ya 64 4月   3 21:22 0 -> pipe:[3852257]
l-wx------. 1 ya ya 64 4月   3 21:22 1 -> pipe:[3852259]
lrwx------. 1 ya ya 64 4月   3 21:17 2 -> /dev/pts/5

$ ls -l /proc/pid_of_wc/fd

lr-x------. 1 ya ya 64 4月   3 21:22 0 -> pipe:[3852259]
lrwx------. 1 ya ya 64 4月   3 21:22 1 -> /dev/pts/5
lrwx------. 1 ya ya 64 4月   3 21:17 2 -> /dev/pts/5

Отношения показаны на рисунке ниже.

В Linux функцией создания канала является канал, и общий способ создания канала выглядит следующим образом.

int fd[2];
if (pipe(fd) < 0) {
    printf("%s\n", "pipe error");
    exit(1);
}

Функция канала создает канал и возвращает два файловых дескриптора, fd[0] используется для чтения данных из канала, а fd[1] используется для записи данных в канал.Далее, давайте посмотрим на фрагмент кода, чтобы увидеть родительский и дочерний процессы Как общаться через каналы.

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

#define  BUF_SIZE 20
int main() {
  int fd[2];
  if (pipe(fd) < 0) {
    printf("%s\n", "pipe error");
    exit(1);
  }
  int pid;
  if ((pid = fork()) < 0) {
    printf("%s\n", "fork error");
    exit(1);
  }

  // child process
  if (pid == 0) {
    close(fd[0]); // 关闭子进程的读
    while (1) {
      int n = write(fd[1], "hello from child\n", 18);
      if (n < 0) {
        printf("write eof\n");
        exit(1);
      }
      sleep(1);
    }
  }

  char buf[BUF_SIZE];
  // parent process
  if (pid > 0) {
    close(fd[1]); // 关闭父进程的写
    while (1) {
      int n = read(fd[0], buf, BUF_SIZE);
      if (n <= 0) {
        printf("read error\n");
        exit(1);
      }
      printf("read from parent: %s", buf);
      sleep(1);
    }
  }
  return 0;
}

Выполнив приведенный выше код, вы увидите строку, написанную дочерним процессом, которую можно прочитать в родительском процессе и отобразить в терминале.

$ ./pipe_test
read from parent: hello from child
read from parent: hello from child
read from parent: hello from child
read from parent: hello from child
read from parent: hello from child

докер и стандартный ввод

Если вы хотите, чтобы stdin процесса docker стал клавиатурным терминалом, вы можете запустить docker run с параметром -it. После запуска образа еще раз проверьте список файловых дескрипторов, открытых процессом, и вы увидите, что все stdin, stdout и stderr изменились, как показано ниже.

$ docker exec -it 5fe22fbffe81 ls -l /proc/1/fd

total 0
lrwx------ 1 root root 64 4月   5 23:20 0 -> /dev/pts/0
lrwx------ 1 root root 64 4月   5 23:20 1 -> /dev/pts/0
lrwx------ 1 root root 64 4月   5 23:20 2 -> /dev/pts/0

Процесс Java также блокируется при вызове System.in.read().

резюме

В этой статье на небольшом примере представлены три основных файловых дескриптора, связанных с процессом: stdin, stdout, stderr, а также то, как перенаправляются эти три файловых дескриптора. Кстати понятие связанное с пайплайном я ввел.Ну и ботинки полные и спать.

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