LINUX.ORG.RU

Читать не прочитать proc-файл

 ,


0

3

Наткнулся тут на странную ошибку в своем коде. Код достаточно банальный, просто читает файл из /proc. Покопался глубже и открыл вот такое поведение. Если читать, скажем, /proc/cpuinfo побайтно, байт за байтом, то все ожидаемо работает и мы вычитаем весь файл до конца. Такое же поведение наблюдаем и с /proc/self/cmdline. Но если попробовать такой же трюк на /proc/sys/net/ipv4/ip_local_port_range то нам удастся прочитать лишь первый байт – последующий вызов read() вернет EOF.

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

Прикол номер два. Казалось бы, вполне корректный жаба-код Files.readString(Path.of("/proc/sys/net/ipv4/ip_local_port_range")) получается сломан. Через свою внутреннюю машинерию он начинает чтение с одного байта, без буфера.

Никакого упоминания про данную gotcha’у ни в мане, ни в документации Ядра я не нашел.

★★★★★

хз всё работает, читать такое побайтно ненужно

#include <fstream>
#include <iostream>

int main()
{
    std::ifstream ifs("/proc/sys/net/ipv4/ip_local_port_range");

    if(ifs)
    {
        int min, max;
        ifs >> min >> max;

        std::cout << "min: " << min << ", max: " << max << std::endl;
    }

    return 0;
}

я так /proc/loadavg парсил без проблем, а там double значения.

anonymous2 ★★★★★
()
Последнее исправление: anonymous2 (всего исправлений: 2)

Файлы имеющие нулевой размер вываливают всё что нужно за 1 операцию чтения, вроде так это работает.

Те у которых есть размер можно читать как обычно.

sergej ★★★★★
()

Да, это «недоработка», но в целом пофиг. Про cpuinfo/cmdline - подозреваю что /proc/sys, который sysctl, систематически отличается от остальных записей в /proc, потому что sysctl-чтение исторически делается только целиком. А может это и просто совпадение. В целом создавать для всех без разбора sysctl-ей временный буфер в ядре - не очень хорошая идея, там могут быть какие-то тяжёлые данные, которые будут тайком тратить память. Да и вообще зачем возиться с этим буфером и усложнять ядро, когда можно буферизовать в юзерспейсе? В джаве нет функции для чтения sysctl?

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)

Ну очевидно баг в ядре.

vbr ★★★★
()

Понятно, что за файлом стоит динамическая структура и вот это вот все, но мне оно выглядит как плохая реализация в Ядре.

В FreeBSD есть нормальный sysctlbyname. Я хз, почему в линуксе его удалили.

Skullnet ★★★★★
()

Если бы ты хоть раз написал бы какой-нибудь модуль для ядра и у него был бы интерфейс через procfs, то ты бы не задавал такие вопросы.

Код в ядре расчитан на здравый смысл, т.е. читать нужно с начала файла и до конца. Позиционирование запрещено. Такой подход сильно упрощает код в ядре.

vel ★★★★★
()

посмотреть в ядре как реализовано выдача этого файла ?? мож там действительно побайтно не прочитать.
«ну так реализовали…» :)

pfg ★★★★★
()
Последнее исправление: pfg (всего исправлений: 1)
Ответ на: комментарий от anonymous2

хз всё работает, читать такое побайтно ненужно

Не нужно, но вот из-за нулевого размера файла такое поведение есть в Жабе.

urxvt ★★★★★
() автор топика
Ответ на: комментарий от sergej

Файлы имеющие нулевой размер вываливают всё что нужно за 1 операцию чтения, вроде так это работает.

Не похоже. Тот же /proc/cpuinfo имеет нулевой размер, но вот читать позволяет как угодно.

urxvt ★★★★★
() автор топика
Ответ на: комментарий от firkax

Согласен, буфер это потенциальный DoS. Но вот возвращать ошибку вместо EOF было бы гораздо лучше, как мне кажется.

Просто забавный случай, когда абстракция протекла от ядра, через все наслоение, аж до Java API. Все есть файл, да не совсем все. Точней, не совсем файл. :)

В джаве нет функции для чтения sysctl?

Она же декларируется как write once run everywhere.

urxvt ★★★★★
() автор топика
Ответ на: комментарий от vel

Да мне, в принципе, это как бы понятно. Не понятно только одно — почему EOF вместо EINVAL (как пример)?

urxvt ★★★★★
() автор топика
Ответ на: комментарий от Skullnet

В FreeBSD ещё /proc задепрекейтиили давно. В линуксе всё не как у людей, proc продвигают, нормальные сисколлы портят.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от vel

Я читаю *сначала*, просто с маленьким буфером. Это не нарушает файловое API и полностью ему соответствует.

urxvt ★★★★★
() автор топика

И я, конечно же, это утверждение проверять «не буду»:

❯ ipython
Python 3.12.3 (main, Apr 23 2024, 09:16:07) [GCC 13.2.1 20240417]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.24.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: f=open('/proc/sys/net/ipv4/ip_local_port_range')

In [2]: f.read(1)
Out[2]: '3'

In [3]: f.read(1)
Out[3]: '2'

In [4]: f.read(1)
Out[4]: '7'

In [5]: f.read(1)
Out[5]: '6'

In [6]: f.read(1)
Out[6]: '8'

In [7]: f.read(1)
Out[7]: '\t'

In [8]: f.read(1)
Out[8]: '6'

In [9]: f.read(1)
Out[9]: '0'

In [10]: f.read(1)
Out[10]: '9'
rtxtxtrx
()
Ответ на: комментарий от rtxtxtrx

И зачем ты проверяешь питоновскую стандартную библиотеку с буферизованным чтением? Речь про сисколл read().

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

питоновскую стандартную библиотеку

In [11]: f=open('/proc/sys/net/ipv4/ip_local_port_range', 'rb', buffering=0)

In [12]: f.read(1)
Out[12]: b'3'

In [13]: f.read(1)
Out[13]: b''

Ну так действительно не работает. И что это доказывает?

rtxtxtrx
()
Ответ на: комментарий от rtxtxtrx

Можно и на Питоне.

$ python   
Python 3.11.9 (main, Apr 10 2024, 13:16:36) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> f = open('/proc/sys/net/ipv4/ip_local_port_range', buffering=0, mode='rb')
>>> f.read(1)
b'3'
>>> f.read(1)
b''
>>> 

urxvt ★★★★★
() автор топика
Последнее исправление: urxvt (всего исправлений: 1)
Ответ на: комментарий от urxvt

Второе чтение уже не с начала файла.

vel ★★★★★
()
Ответ на: комментарий от firkax

Да, какашка. Вот написал сниппет:

import java.io.*;
import java.nio.file.Files;

public class sample
{	
	public static void main(String[] args)
	{
		byte[] fileContent = new byte[16];
		try (InputStream in = new FileInputStream(new File("/proc/sys/net/ipv4/ip_local_port_range"))) {
			in.read(fileContent);
		} catch (Exception e) {
			e.printStackTrace();
		}

		String test = new String(fileContent);
		String[] parts = test.replace("\n", "").split("\t");
		System.out.println(parts[0]);
		System.out.println(parts[1]);
	}
}

Он работает, но прикол в том что:

  1. Нужно угадать буффер так как строка, а размер файла = 0 байт (через чтение всего файла читает только 1 байт), вместо того чтобы получить 4 байта (т.е сделать short[2]) через sysctl как в фри (окей, в freebsd два разных значения, но их можно прочитать как short).
  2. Нужно парсить и удалять перенос строки в конце, а потом сплит по табуляции между значениями.
  3. Потом конвертация в число.
Skullnet ★★★★★
()
Последнее исправление: Skullnet (всего исправлений: 2)

Но зато Files.readAllLines работает - читает всё.

Ja-Ja-Hey-Ho ★★★★★
()
Ответ на: комментарий от urxvt

Возможно, но cpuinfo это статический буфер, который не меняется.

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

sergej ★★★★★
()
Ответ на: комментарий от sergej

Возможно, но cpuinfo это статический буфер, который не меняется.

Да вот, тоже, не совсем. Так у меня поле `cpu MHz` в нем меняется.

urxvt ★★★★★
() автор топика
Ответ на: комментарий от peregrine

как это бесконечность? сколько попросишь столько и вывалит или чуть меньше

sergej ★★★★★
()
Ответ на: комментарий от peregrine
$ ls -l /dev/urandom 
crw-rw-rw- 1 root root 1, 9 мая 30 23:25 /dev/urandom
$ ls -l /proc/sys/net/ipv4/ip_local_port_range
-rw-r--r-- 1 root root 0 июн  1 14:44 /proc/sys/net/ipv4/ip_local_port_range

Буковку с видишь?

wandrien ★★
()
Ответ на: комментарий от vel

читать нужно с начала файла и до конца.

Такого сискола/флагов ведь нет? Чтобы вернуть ошибку, если до конца не прочиталось? Тогда получается, что признаком полноты прочитаных данных будет, что первый (единственный) read() вернул байт меньше, чем размер буфера. Иначе, нужно сделать буфер больше и повторить чтение.

И об этом нигде не написано, нигде не написано, как это делать в ЯП высокого уровня. Нигде не написано, что cat /proc/PID/smaps неправильно, правильно только dd...

mky ★★★★★
()
Ответ на: комментарий от mky

Еще один теоретик...

Ленивые жопы разработчики написали код:

kernel/sysctl.c:

static int __do_proc_dointvec(void *tbl_data, struct ctl_table *table, int write, void *buffer, 

...
        if (!tbl_data || !table->maxlen || !*lenp || (*ppos && !write)) {
                *lenp = 0;
                return 0;
        }

Если читаем (!write) и pos != 0, то вот вам EOF.

Уверен, что этому коду дохрена лет.

И об этом нигде не написано, нигде не написано, как это делать в ЯП высокого уровня. Нигде не написано, что cat /proc/PID/smaps неправильно, правильно только dd...

В Documentation/filesystems/proc.rst намекают

Note: reading /proc/PID/maps or /proc/PID/smaps is inherently racy (consistent output can be achieved only in the single read call).

Под лежачий камень ничего не течёт.

Уверен, что если кто-то из вас оторвёт свою задницу от уютного кресла/дивана и запостит патч в linux-kernel для корректного чтения данных из procfs, то его с радостью примут.

Но ещё более вероятно, что просто впишут в документацию что читать с начала и до конца :)

vel ★★★★★
()
Последнее исправление: vel (всего исправлений: 1)
Ответ на: комментарий от vel

Смысл писать патч для ядра, если smaps парсят здесь и сейчас? Куча скриптов и, получается, неправильные. Это в документацию нужно писать патч с примером корректного чтения с while()/malloc()/read() и с while dd для bash.

В Documentation/filesystems/proc.rst намекают

Тем не менее, cat читает smaps за несколько read(), и на каждый ему ядро что-то возвращает.

читать с начала и до конца :)

У вас там всё хорошо? Я вам пишу, что такого syscall не существует, а вы опять про pos != 0. В man read() прямо написано, что чтение до конца — это чтение пока read() не вернёт 0.

Надеюсь, что разработчики не додумаются вписать «read the file from the beginning till the end». «single read call» правильнее, но могли бы уточнить, single после pos==0, то есть можно seek(0), нет необходимости close()/open(), если буфера не хватило...

mky ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.