Русский
Русский
English
Статистика
Реклама

Системные вызовы

Сетевики нужны и вот почему

30.10.2020 12:22:53 | Автор: admin

Картинка взята из телнет-видео Звёздных войн: telnet towel.blinkenlights.nl

Недавно был пост о том, нужны ли сетевики. До тех пор, пока проверка доступности tcp/ip порта кажется чем-то сложным даже для администраторов БД и AD, сетевики несомненно нужны. Они особенно полезны в тех случаях, когда необходимо понять почему так плохо работает некое клиент-серверное приложение ценой в пароход.
Иногда мало знать ping и traceroute для того, чтобы понять и устранить проблему в сети. Необходимо понимать как работают все звенья в цепи, а сделать это может лишь сетевик. Рассмотрим несколько таких примеров.

Чем Netcat лучше telnet?


Практически всегда, если речь идет о проверке TCP/UDP порта стараются сделать это через telnet и приходится всякий раз просить поставить Netcat. Есть несколько причин, по которым telnet сильно проигрывает. Вот основные причины.

  • telnet не умеет различать причины недоступности порта. Все, или ничего connect, либо fail. Netcat может подсказать, в чем проблема. В этом случае все просто, такого порта в состоянии LISTENING нет.
  • Не умеет в UDP, SCTP и прочие, ничего кроме TCP.
  • Не имеет опций тайм-аута соединения -w и проверки без подключения -z

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

(1:1004)$ ncat -v -w3 -z some.server 1111Ncat: Version 7.80 ( https://nmap.org/ncat )Ncat: Connection refused.

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

(1:1005)$ ncat -v -w3 -z another.server 1521Ncat: Version 7.80 ( https://nmap.org/ncat )Ncat: No route to host.

Бытует мнение, что на Windows нет альтернативы telnet, однако это неверно. Существует штатная утилита Test-NetConnection, которую можно запустить из PowerShell.

Test-NetConnection -ComputerName "www.contoso.com" -Port 80

Linux MIB в помощь


Если ваш сервер вдруг перестал реагировать на внешние раздражители и клиентские запросы стали провисать, не всегда причина в выдернутом сетевом кабеле, или зависших сервисах JBoss/Tomcat. Прежде, чем заводить кейс в тех-поддержку проверьте Linux MIB. Есть множество программ, выдающих статистику ядра Linux по внутренним счетчикам SNMP. Самая распространенная из них все еще netstat.

(1:1008)$ netstat -s |grep prune873 packets pruned from receive queue because of socket buffer overrun(1:1009)$ nstat -s |grep PruneTcpExtPruneCalled   873 0.0

Если сервер не справляется с сетевой нагрузкой и буфер сокета перманентно переполнен, тогда ядро будет просто сбрасывать все новые пакеты и счетчик будет расти на глазах. Чтобы исправить эту ситуацию, можно добавить некоторые настраиваемые параметры в файле sysctl.conf. Текущие значения можно увидеть из файловой системы /proc.

(1:1010)$ cat /proc/sys/net/ipv4/tcp_rmem4096    16384   4194304(1:1011)$ cat /proc/sys/net/ipv4/wcp_rmem4096    16384   4194304(1:1012)$ grep -e tcp_rmem -e tcp_wmem /etc/sysctl.confnet.ipv4.tcp_rmem = 4096 87380 16777216net.ipv4.tcp_wmem = 4096 65536 16777216

Три значения параметра tcp_rmem/tcp_wmem обозначает соответственно минимальное, пороговое и максимальное значение. По умолчанию величины выставлены довольно разумно, поэтому менять их без веских оснований не стоит. Чаще всего такая ситуация может возникнуть на сети с пропускной способностью 10 GiB/s. После сохранения изменений в файле sysctl.conf следует выполнить systcl -p. Эта команда запускает системный вызов setsockopt (SO_RCVBUF).
Следует отметить, что буфер setsockopt(SO_RCVBUF), установленный в приложении, будет ограничен значениями, установленными в net.core.rmem_default и net.core.rmem_max и если приложение имеет буфер размером 1 мб, но net.core.rmem_max составляет всего 256 кб, то буфер сокета приложения будет ограничен этим значением.

Диагностика сети с помощью системных вызовов


Для любого сетевика WireShark абсолютно необходимый инструмент отладки, иногда главный. Не раз и не два только благодаря WireShark удавалось спасти проект в очень сложной ситуации. Но бывают случаи, когда только в системных вызовах ядра можно увидеть сетевую проблему.
В частности отброшенные пакеты нельзя будет увидеть в tcpdump, или WireShark. Зато можно их увидеть в режиме реального времени с помощью tcpdrop из сета bcc-tools. У команды нет никаких опций, просто выполните tcpdop. Вывод состоит из сокета, статусе соединения и флагах TCP. Далее идет трассировка стека ядра, которая привела к этому сбросу пакета.

(1:1013)$ /usr/share/bcc/tools/tcpdropTIME     PID    IP SADDR:SPORT  > DADDR:DPORT  STATE (FLAGS)16:31:07 3103   4  93.184.216.34:443       > 192.168.11.32:36222   ESTABLISHED (PSH|ACK)b'tcp_drop+0x1'b'tcp_data_queue+0x1e7'b'tcp_rcv_established+0x1df'b'tcp_v4_do_rcv+0x131'b'tcp_v4_rcv+0xad3'b'ip_protocol_deliver_rcu+0x2b'b'ip_local_deliver_finish+0x55'b'ip_local_deliver+0xe8'b'ip_rcv+0xcf'b'__netif_receive_skb_core+0x429'b'__netif_receive_skb_list_core+0x10a'b'netif_receive_skb_list_internal+0x1bf'b'netif_receive_skb_list+0x26'b'iwl_pcie_rx_handle+0x447'b'iwl_pcie_irq_handler+0x4d5'b'irq_thread_fn+0x20'b'irq_thread+0xdc'b'kthread+0x113'b'ret_from_fork+0x35'

В bcc-tools имеется еще одна очень полезная утилита tcplife, которая показывает параметры TCP соединение, в том числе, длительность сеанса. У этой команды есть некоторые опции.

(1:1014)$ /usr/share/bcc/tools/tcplife -h...examples:    ./tcplife           # trace all TCP connect()s    ./tcplife -T        # include time column (HH:MM:SS)    ./tcplife -w        # wider columns (fit IPv6)    ./tcplife -stT      # csv output, with times & timestamps    ./tcplife -p 181    # only trace PID 181    ./tcplife -L 80     # only trace local port 80    ./tcplife -L 80,81  # only trace local ports 80 and 81    ./tcplife -D 80     # only trace remote port 80

Просмотр сессий выглядит так.

# ./tcplife -D 80PID   COMM       LADDR           LPORT RADDR           RPORT TX_KB RX_KB MS27448 curl       100.66.11.247   54146 54.154.224.174  80        0     1 263.8527450 curl       100.66.11.247   20618 54.154.164.22   80        0     1 243.6227452 curl       100.66.11.247   11480 54.154.43.103   80        0     1 231.1627454 curl       100.66.11.247   31382 54.154.15.7     80        0     1 249.95

Может так случится, что между клиентом и сервером расположен межсетевой экран, либо IPS/IDS с набором правил по обрыву длительности TCP сессии по прошествии определенного времени, или достижении некоего объёма трафика. В этом случае диагностика подобного кейса может затянутся неопределенно долго, так как находится в слепой зоне, как со стороны клиентского, так и со стороны серверного приложения. Единственный способ понять в чему тут дело, это использовать инструменты bcc-tools.

Недооцененный резолвинг


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

getnameinfo(getaddrinfo(HOSTNAME)) = HOSTMANE

ункции getnameinfo и getaddrinfo являются заменой устаревших gethostbyname и gethostbyaddr соответственно.

int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,    char *host, socklen_t hostlen,    char *serv, socklen_t servlen, int flags);int getaddrinfo(const char *node, const char *service,    const struct addrinfo *hints, struct addrinfo **res);

Положительный пример lib.ru

(1:1015)$ host lib.rulib.ru has address 81.176.66.163...(1:1016)$ host 81.176.66.163163.66.176.81.in-addr.arpa domain name pointer lib.ru.Отрицательный пример - example.com.(1:1017)$ host example.comexample.com has address 93.184.216.34(1:1018)$ host 93.184.216.34Host 34.216.184.93.in-addr.arpa. not found: 3(NXDOMAIN)


Если бы IP 93.184.216.34 указывал не на example.com, а на иной хост, то и это было бы ошибкой. Только тождество результат двустороннего разрешения имен гарантирует отсутствие ошибок настройки.
Вообще-то используя утилиту host следует помнить, что она так же, как dig и nslookup используют лишь DNS для разрешения имён хостов и игнорируют записи в файле /etc/hosts. По этой причине целесообразнее использовать команду getent.

(1:1019)$ strace -f -e trace=openat host my.router 2>&1 |grep -e "^openat" -e addressopenat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/libcrypto.so.1.1", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/libuv.so.1", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/libidn2.so.0", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/lib64/libz.so.1", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/libunistring.so.2", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/proc/self/task/7164/comm", O_RDWR) = 3(1:1020)$ strace -f -e trace=openat getent ahosts my.router openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/gconv/gconv-modules.cache", O_RDONLY) = 3openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/etc/host.conf", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/lib64/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/libidn2.so.0", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/libunistring.so.2", O_RDONLY|O_CLOEXEC) = 3openat(AT_FDCWD, "/usr/lib64/charset.alias", O_RDONLY|O_NOFOLLOW) = -1 ENOENT (No such file or directory)192.168.10.10    STREAM my.router192.168.10.10    DGRAM  192.168.10.10    RAW    +++ exited with 0 +++


Из вывода видно, что host не обращается к файлу /etc/hosts и по этой причине не в состоянии определить IP адрес маршрутизатора. Напротив, getent ищет в этом файле и находит соответствующую запись.

Использованные материалы






Подробнее..

Перевод Реализация epoll, часть 2

05.11.2020 16:06:36 | Автор: admin
Публикуя перевод первой статьи из цикла материалов о реализации epoll, мы провели опрос, посвящённый целесообразности перевода продолжения цикла. Более 90% участников опроса высказались за перевод остальных статей. Поэтому сегодня мы публикуем перевод второго материала из этого цикла.



Функция ep_insert()


Функция ep_insert() это одна из самых важных функций в реализации epoll. Понимание того, как она работает, чрезвычайно важно для того чтобы разобраться в том, как именно epoll получает сведения о новых событиях от файлов, за которыми наблюдает.

Объявление ep_insert() можно найти в строке 1267 файла fs/eventpoll.c. Рассмотрим некоторые фрагменты кода этой функции:

user_watches = atomic_long_read(&ep->user->epoll_watches);if (unlikely(user_watches >= max_user_watches))  return -ENOSPC;

В этом фрагменте кода функция ep_insert() сначала проверяет, не превышает ли общее количество файлов, за которым наблюдает текущий пользователь, значения, заданного в /proc/sys/fs/epoll/max_user_watches. Если user_watches >= max_user_watches, то функция немедленно прекращает работу с errno, установленным в ENOSPC.

После этого ep_insert() выделяет память, пользуясь механизмом управления памятью slab ядра Linux:

if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))  return -ENOMEM;

Если функции удалось выделить объём памяти, достаточный для struct epitem, будет выполнен следующий процесс инициализации:

/* Инициализация ... */INIT_LIST_HEAD(&epi->rdllink);INIT_LIST_HEAD(&epi->fllink);INIT_LIST_HEAD(&epi->pwqlist);epi->ep = ep;ep_set_ffd(&epi->ffd, tfile, fd);epi->event = *event;epi->nwait = 0;epi->next = EP_UNACTIVE_PTR;

После этого ep_insert() попытается зарегистрировать коллбэк в файловом дескрипторе. Но прежде чем мы сможем об этом поговорить, нам нужно познакомиться с некоторыми важными структурами данных.

Структура poll_table это важная сущность, используемая реализацией poll() VFS. (Понимаю, что в этом можно запутаться, но тут мне хотелось бы объяснить, что функция poll(), которую я тут упомянул, представляет собой реализацию файловой операции poll(), а не системный вызов poll()). Она объявлена в include/linux/poll.h:

typedef struct poll_table_struct {  poll_queue_proc _qproc;  unsigned long _key;} poll_table;

Сущность poll_queue_proc представляет тип функции-коллбэка, который выглядит так:

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

Член _key таблицы poll_table, на самом деле, является не тем, чем он может показаться на первый взгляд. А именно, несмотря на имя, наводящее на мысль о некоем ключе, в _key, на самом деле, хранятся маски интересующих нас событий. В реализации epoll _key устанавливается в ~0 (дополнение до 0). Это значит, что epoll стремится получать сведения о событиях любых видов. В этом есть смысл, так как приложения пользовательского пространства могут в любое время менять маску событий с помощью epoll_ctl(), принимая все события от VFS, а затем фильтруя их в реализации epoll, что упрощает работу.

Для того чтобы облегчить восстановление в poll_queue_proc исходной структуры epitem, epoll использует простую структуру, называемую ep_pqueue, которая служит обёрткой для poll_table с указателем на соответствующую структуру epitem (файл fs/eventpoll.c, строка 243):

/* Структура-обёртка, используемая в очереди */struct ep_pqueue {  poll_table pt;  struct epitem *epi;};

Затем ep_insert() инициализирует struct ep_pqueue. Следующий код сначала записывает в член epi структуры ep_pqueue указатель на структуру epitem, соответствующую файлу, который мы пытаемся добавить, а затем записывает ep_ptable_queue_proc() в член _qproc структуры ep_pqueue, а в _key записывает ~0.

/* Инициализация таблицы с использованием коллбэка */epq.epi = epi;init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

После этого ep_insert() вызовет ep_item_poll(epi, &epq.pt);, что приведёт к вызову реализации poll(), связанной с файлом.

Давайте рассмотрим пример, в котором используется реализация poll() TCP-стека Linux, и разберёмся с тем, что именно эта реализация делает с poll_table.

Функция tcp_poll() это реализация poll() для TCP-сокетов. Её код можно найти в файле net/ipv4/tcp.c, в строке 436. Вот фрагмент этого кода:

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait){  unsigned int mask;  struct sock *sk = sock->sk;  const struct tcp_sock *tp = tcp_sk(sk);  sock_rps_record_flow(sk);  sock_poll_wait(file, sk_sleep(sk), wait);  // код опущен}

Функция tcp_poll() вызывает sock_poll_wait(), передавая, в качестве второго аргумента, sk_sleep(sk), а в качестве третьего wait (это ранее переданная функции tcp_poll() таблица poll_table).

А что представляет собой sk_sleep()? Как оказывается, это всего лишь геттер, предназначенный для доступа к очереди ожидания событий для конкретной структуры sock (файл include/net/sock.h, строка 1685):

static inline wait_queue_head_t *sk_sleep(struct sock *sk){  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);  return &rcu_dereference_raw(sk->sk_wq)->wait;}

Что же sock_poll_wait() собирается делать с очередью ожидания событий? Оказывается, что эта функция выполнит некую простую проверку и потом вызовет poll_wait() с передачей тех же самых параметров. Затем функция poll_wait() вызовет заданный нами коллбэк и передаст ему очередь ожидания событий (файл include/linux/poll.h, строка 42):

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p){  if (p && p->_qproc && wait_address)    p->_qproc(filp, wait_address, p);}

В случае с epoll сущность _qproc будет представлять собой функцию ep_ptable_queue_proc(), объявленную в файле fs/eventpoll.c, в строке 1091.

/** Это - коллбэк, используемый для включения нашей очереди ожидания в* состав списков процессов целевого файла, работу которых надо возобновить.*/static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,       poll_table *pt){  struct epitem *epi = ep_item_from_epqueue(pt);  struct eppoll_entry *pwq;  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);    pwq->whead = whead;    pwq->base = epi;    add_wait_queue(whead, &pwq->wait);    list_add_tail(&pwq->llink, &epi->pwqlist);    epi->nwait++;  } else {    /* Нам нужно сообщить о возникновении ошибки */    epi->nwait = -1;  }}

Сначала ep_ptable_queue_proc() пытается восстановить структуру epitem, которая соответствует файлу из очереди ожидания, с которым мы работаем. Так как epoll использует структуру-обёртку ep_pqueue, восстановление epitem из указателя poll_table представлено простой операцией с указателями.

После этого ep_ptable_queue_proc() просто выделяет столько памяти, сколько нужно для struct eppoll_entry. Эта структура работает как связующее звено между очередью ожидания файла, за которым ведётся наблюдение, и соответствующей структурой epitem для этого файла. Для epoll чрезвычайно важно знать о том, где находится голова очереди ожидания для файла, за которым ведётся наблюдение. В противном случае epoll не сможет позже отменить регистрацию в очереди ожидания. Структура eppoll_entry, кроме того, включает в себя очередь ожидания (pwq->wait) с функцией возобновления работы процесса, представленной ep_poll_callback(). Возможно, pwq->wait это самая важная часть во всей реализации epoll, так как эту сущность используют для решения следующих задач:

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

После этого ep_ptable_queue_proc() присоединит pwq->wait к очереди ожидания целевого файла (whead). Функция, кроме того, добавит struct eppoll_entry в связный список из struct epitem (epi->pwqlist) и инкрементирует значение epi->nwait, представляющее собой длину списка epi->pwqlist.

А вот тут у меня возникает один вопрос. Почему epoll нужно использовать связный список для хранения структуры eppoll_entry внутри структуры epitem одного файла? Не нужен ли epitem лишь один элемент eppoll_entry?

Я, правда, не могу точно ответить на этот вопрос. Насколько я могу судить, если только некто не собирается использовать экземпляры epoll в каких-нибудь безумных циклах, список epi->pwqlist будет содержать лишь один элемент struct eppoll_entry, а epi->nwait для большинства файлов, скорее всего, будет равняться 1.

Хорошо тут то, что неясности вокруг epi->pwqlist никак не отражаются на том, о чём я буду говорить ниже. А именно, речь пойдёт о том, как Linux уведомляет экземпляры epoll о событиях, происходящих с файлами, за которыми осуществляется наблюдение.

Помните то, о чём мы говорили в предыдущем разделе? Речь шла о том, что epoll присоединяет wait_queue_t к списку ожидания целевого файла (к wait_queue_head_t). Несмотря на то, что wait_queue_t чаще всего используется как механизм возобновления работы процессов, это, по сути, просто структура, хранящая указатель на функцию, которая будет вызвана тогда, когда Linux решит возобновить работу процессов из очереди wait_queue_t, прикреплённую к wait_queue_head_t. В этой функции epoll может принять решение о том, что делать с сигналом возобновления работы, но у epoll нет необходимости возобновлять работу какого-либо процесса! Как можно будет увидеть позже, обычно при вызове ep_poll_callback() возобновления работы чего-либо не происходит.

Полагаю, ещё стоит обратить внимание на то, что механизм возобновления работы процессов, применяемый в poll(), полностью зависит от реализации. В случае с файлами TCP-сокетов голова очереди ожидания это член sk_wq, сохранённый в структуре sock. Это, кроме того, объясняет необходимость использования коллбэка ep_ptable_queue_proc() для работы с очередью ожидания. Так как в реализациях очереди для разных файлов голова очереди может оказаться в совершенно разных местах, у нас нет способа обнаружить нужное нам значение wait_queue_head_t без использования коллбэка.

Когда именно осуществляется возобновление работы sk_wq в структуре sock? Как оказалось, система сокетов Linux следует тем же OO-принципам проектирования, что и VFS. Структура sock объявляет следующие хуки в строке 2312 файла net/core/sock.c:

void sock_init_data(struct socket *sock, struct sock *sk){  // код опущен...  sk->sk_data_ready  =   sock_def_readable;  sk->sk_write_space =  sock_def_write_space;  // код опущен...}

В sock_def_readable() и sock_def_write_space() осуществляется вызов wake_up_interruptible_sync_poll() для (struct sock)->sk_wq с целью выполнения функции-коллбэка, возобновляющей работу процесса.

Когда же будут вызываться sk->sk_data_ready() и sk->sk_write_space()? Это зависит от реализации. Рассмотрим, в качестве примера, TCP-сокеты. Функция sk->sk_data_ready() будет вызвана во второй половине обработчика прерывания в том случае, когда TCP-подключение завершит процедуру трёхстороннего рукопожатия, или когда для некоего TCP-сокета будет получен буфер. Функция sk->sk_write_space() будет вызвана при изменении состояния буфера с full на available. Если помнить об этом при разборе следующих тем, особенно той, что посвящена срабатыванию по фронту, эти темы будут выглядеть интереснее.

Итоги


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

Пользовались ли вы epoll?



Подробнее..

Перевод Реализация epoll, часть 4

09.11.2020 20:17:29 | Автор: admin
Это последний материал из серии четырёх статей (часть 1, часть 2, часть 3), посвящённой реализации epoll. Тут речь пойдёт о том, как epoll передаёт события из пространства ядра в пользовательское пространство, и о том, как реализованы режимы срабатывания по фронту и по уровню.



Эта статья написана позже остальных. Когда я начинал работу над первым материалом, самой свежей стабильной версией ядра Linux была 3.16.1. А во время написания данной статьи это уже версия 4.1. Именно на коде этой версии ядра и основана данная статья. Код, правда, изменился не особенно сильно, поэтому читатели предыдущих статей могут не беспокоиться о том, что что-то в реализации epoll очень сильно изменилось.

Взаимодействие с пользовательским пространством


В предыдущих материалах я потратил довольно много времени на объяснение того, как работает система обработки событий в ядре. Но, как известно, ядру надо передать сведения о событиях программе, работающей в пользовательском пространстве для того чтобы программа могла бы воспользоваться этими сведениями. Это, в основном, делается с помощью системного вызова epoll_wait(2).

Код этой функции можно найти в строке 1961 файла fs/eventpoll.c. Сама эта функция очень проста. После вполне обычных проверок она просто получает указатель на eventpoll из файлового дескриптора и выполняет вызов следующей функции:

error = ep_poll(ep, events, maxevents, timeout);

Функция ep_poll()


Функция ep_poll() объявлена в строке 1585 того же файла. Она начинается с проверки того, задал ли пользователь значение timeout. Если так и было, то функция инициализирует очередь ожидания и устанавливает тайм-аут в значение, заданное пользователем. Если пользователь не хочет ждать, то есть, timeout = 0, то функция сразу же переходит к блоку кода с меткой check_events:, ответственному за копирование события.

Если же пользователь задал значение timeout, и событий, о которых ему можно сообщить, нет (их наличие определяют с помощью вызова ep_events_available(ep)), функция ep_poll() добавляет сама себя в очередь ожидания ep->wq (вспомните то, о чём мы говорили в третьем материале этой серии). Там мы упоминали о том, что ep_poll_callback() в процессе работы активирует любые процессы, ожидающие в очереди ep->wq.

Затем функция переходит в режим ожидания, вызывая schedule_hrtimeout_range(). Вот в каких обстоятельствах спящий процесс может проснуться:

  1. Истекло время тайм-аута.
  2. Процесс получил сигнал.
  3. Возникло новое событие.
  4. Ничего не произошло, а планировщик просто решил активировать процесс.

В сценариях 1, 2 и 3 функция устанавливает соответствующие флаги и выходит из цикла ожидания. В последнем случае функция просто снова переходит в режим ожидания.

После того, как эта часть работы сделана, ep_poll() продолжает выполнять код блока check_events:.

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

ep_send_events(ep, events, maxevents)

Функция ep_send_events() объявлена в строке 1546. Она, после вызова, вызывает функцию ep_scan_ready_list(), передавая, в качестве коллбэка, ep_send_events_proc(). Функция ep_scan_ready_list() проходится в цикле по списку готовых файловых дескрипторов и вызывает ep_send_events_proc() для каждого найденного ей готового события. Ниже станет понятно, что механизм, предусматривающий применение коллбэка, нужен для обеспечения безопасности и многократного использования кода.

Функция ep_send_events() сначала помещает данные из списка готовых файловых дескрипторов структуры eventpool в свою локальную переменную. Затем она устанавливает поле ovflist структуры eventpool в NULL (а его значением по умолчанию является EP_UNACTIVE_PTR).

Зачем авторы epoll используют ovflist? Это сделано ради обеспечения высокой эффективности работы epoll! Можно заметить, что после того, как список готовых файловых дескрипторов был взят из структуры eventpool, ep_scan_ready_list() устанавливает ovflist в значение NULL. Это приводит к тому, что ep_poll_callback() не попытается присоединить событие, которое передаётся в пользовательское пространство, обратно к ep->rdllist, что может привести к большим проблемам. Благодаря использованию ovflist функции ep_scan_ready_list() не нужно удерживать блокировку ep->lock при копировании событий в пользовательское пространство. В результате улучшается общая производительность решения.

После этого ep_send_events_proc() обойдёт имеющийся у неё список готовых файловых дескрипторов и снова вызовет их методы poll() для того чтобы удостовериться в том, что событие действительно произошло. Зачем epoll снова проверяет здесь события? Делается это для того чтобы убедиться в том, что событие (или события), зарегистрированное пользователем, всё ещё доступно. Поразмыслите над ситуацией, когда файловый дескриптор был добавлен в список готовых файловых дескрипторов по событию EPOLLOUT в тот момент, когда пользовательская программа выполняет запись в этот дескриптор. После того, как программа завершит запись, файловый дескриптор уже может быть недоступным для записи. Epoll нужно правильно обрабатывать подобные ситуации. В противном случае пользователь получит EPOLLOUT в тот момент, когда операция записи будет заблокирована.

Тут, правда, стоит упомянуть об одной детали. Функция ep_send_events_proc() прилагает все усилия для того чтобы обеспечить получение программами из пространства пользователя точных уведомлений о событиях. При этом возможно, хотя и маловероятно, то, что доступный набор событий изменится после того, как ep_send_events_proc() вызовет poll(). В этом случае программа из пользовательского пространства может получить уведомление о событии, которого больше не существует. Именно поэтому правильным считается всегда использовать неблокирующие сокеты при применении epoll. Благодаря этому ваше приложение не будет неожиданно заблокировано.

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

Срабатывание по фронту и срабатывание по уровню


Теперь мы наконец можем обсудить разницу между срабатыванием по фронту (Edge Triggering, ET) и срабатыванием по уровню (Level Triggering, LT) с точки зрения особенностей их реализации.

else if (!(epi->event.events & EPOLLET)) {    list_add_tail(&epi->rdllink, &ep->rdllist);}

Это очень просто! Функция ep_send_events_proc() добавляет событие обратно в список готовых файловых дескрипторов. В результате при следующем вызове ep_poll() тот же файловый дескриптор будет снова проверен. Так как ep_send_events_proc() всегда вызывает для файла poll() перед возвратом его приложению пользовательского пространства, это немного увеличивает нагрузку на систему (в сравнении с ET) если файловый дескриптор больше не доступен. Но смысл этого всего заключается в том, чтобы, как сказано выше, не сообщать о событиях, которые больше недоступны.

После того, как ep_send_events_proc() завершит копирование событий, функция возвращает количество скопированных ей событий, держа в курсе происходящего приложение пользовательского пространства.

Когда функция ep_send_events_proc() завершила работу, функции ep_scan_ready_list() нужно немного прибраться. Сначала она возвращает в список готовых файловых дескрипторов события, которые остались необработанными функцией ep_send_events_proc(). Такое может произойти в том случае, если количество доступных событий превысит размеры буфера, предоставленного программой пользователя. Кроме того, ep_send_events_proc() быстро прикрепляет все события из ovflist, если таковые имеются, обратно к списку готовых файловых дескрипторов. Далее, в ovflist опять записывается EP_UNACTIVE_PTR. В результате новые события будут прикрепляться к главному списку ожидания (rdllist). Функция завершает работу, активируя любые другие спящие процессы в том случае, если имеются ещё какие-то доступные события.

Итоги


На этом я завершаю четвёртую и последнюю статью из цикла, посвящённого реализации epoll. Я, в процессе написания этих статей, был впечатлён той огромной умственной работой, которую проделали авторы кода ядра Linux для достижения максимальной эффективности и масштабируемости. И я благодарен всем авторам кода Linux за то, что они, выкладывая результаты своей работы в общий доступ, делятся своими знаниями со всеми, кто в них нуждается.

Как вы относитесь к опенсорсному программному обеспечению?



Подробнее..

Утраченный потенциал подсистемы Windows для Linux (WSL)

06.01.2021 10:12:06 | Автор: admin


Если вы несколько лет вообще не следили за Windows 10 и не знаете, что происходит, то пропустили одну вещь очень горячей темой для разработчиков стала подсистема Windows для Linux, она же WSL. Среди программистов очень часто её обсуждают. Действительно, потрясающе интересная штука.

Наконец-то у нас появилась возможность запустить свой инструментарий Linux на Windows наравне с виндовыми программами. А это значит, что больше не нужно изучать странный PowerShell или пользоваться архаичной консолью CMD.EXE.

К сожалению, не всё так радужно. WSL по-прежнему является неким инородным элементом, который отделён от родной среды Windows. В частности, не может взаимодействовать с родными инструментами Windows.

А ведь изначально всё задумывалось совсем иначе, пишет Джулио Мерино (Julio Merino), автор блога для разработчиков jmmv.dev. Подсистема должна была стать совсем другой, но фактически вышел провал, в каком-то смысле.

Чтобы понять причины этого провала, нужно сначала понять различия между WSL 1 и WSL 2 и как переход на WSL 2 закрыл некоторые интересные перспективы.

Обзор архитектуры WSL 1


Давайте сначала взглянем на WSL 1, и первым делом на её странное название. Почему эта функция называется подсистемой Windows для Linux? Разве не наоборот? Это же не подсистема в Linux, которая делает что-то связанное с Windows, а именно наоборот! То есть грамотно она должна называться Подсистема с функциональностью Linux для Windows или Подсистема Linux для Windows или LSW. Откуда путаница?

Есть мнение, что Microsoft была вынуждена выбрать название наоборот, чтобы избежать упоминания чужих торговых марок. Вот слова Рича Тёрнера (Rich Turner), проект-менеджера WSL:


Что ж с другой стороны, такое странное название технически можно считать корректным, если внимательно изучить архитектуру ядра Windows NT. На странице Архитектура Windows NT в Википедии мы находим следующее:



Пользовательский режим Windows NT состоит из подсистем, передающих запросы ввода-вывода соответствующему драйверу режима ядра посредством менеджера ввода-вывода. Есть две подсистемы на уровне пользователя: подсистемы окружения (запускают приложения, написанные для разных операционных систем) и интегрированные или внутренние подсистемы (управляет особыми системными функциями от имени подсистемы окружения). Режим ядра имеет полный доступ к аппаратной части и системным ресурсам компьютера.

Windows NT разработана с нуля для поддержки процессов, запущенных из множества операционных систем, а Win32 был просто одной из этих подсистем окружения. На такой платформе WSL 1 предоставляет новую подсистему окружения, подсистему Linux, для запуска бинарников Linux поверх ядра Windows NT. У подсистем окружения Win32 и Linux должна быть одна общая интегральная подсистема.

Что всё это на самом деле значит?

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

Способ, которым процесс выполняет системные вызовы, и семантика этих системных вызовов специфичны для операционной системы. Например, на старых х86 это выглядит так: открытие файла в системе Win32 системный вызов 17h, который инициируется через прерывание INT 2EH, а при открытии файла в системе Linux это системный вызов 5h, который инициируется через прерывание INT 80х.

Но концептуально открытие файла это открытие файла, верно? Нам особенно не интересно, что номера системных вызовов или номера прерываний отличаются друг от друга. И в этом заключается ключевой аспект дизайна WSL 1: подсистема Linux в ядре NT это, проще говоря, реализация уровня системных вызовов Linux перед ядром NT. Эти системные вызовы позже делегируются примитивам NT, а не вызовам Win32. Самое главное: нет никакого перевода с системных вызовов Linux на системные вызовы Win32.

В каком-то смысле это подвиг архитектурной мысли и реальной программной разработки, учитывая в целом хорошую поддержку Linux-приложений под WSL 1 и помня о множестве реальных внутренних отличий NT от Unix, когда Windows вообще нативно не воспринимает стандартную юниксовую логику forkexec.

Истинная красота этой конструкции заключается в том, что на машине работает одно ядро, и это ядро имеет целостное представление обо всех процессах под ним. Ядро знает всё о процессах Win32 и Linux. И все эти процессы взаимодействуют с общими ресурсами, такими как единый сетевой стек, единый менеджер памяти и единый планировщик процессов.

Причины для создания WSL 2


Если WSL 1 так крут, то зачем нужен WSL 2? Здесь две причины:

  • WSL 1 должен, по сути, реализовать все двоичные интерфейсы приложений (ABI) ядра Linux, как говорится, бит к биту. Если в интерфейсе есть ошибка, WSL 1 должен её воспроизвести. А если есть функция, которую трудно представить в ядре NT, то она либо не может быть реализована, либо нуждается в дополнительной логике ядра (и поэтому становится медленнее).


    Уровни и интерфейсы между ними: API и ABI. Высокоуровневое сравнение

  • Подсистема Linux в WSL 1 должна соблюдать любые ограничения и внутренние различия, существующие между ядром NT и традиционным дизайном Unix. Наиболее очевидным отличием является файловая система NTFS и её семантика, а также то, как эти различия вредят производительности бинарных файлов Linux. Низкая производительность файловой системы, видимо, была распространённой жалобой при использовании WSL 1.

WSL 2 выбрасывает всю часть подсистемы Linux и заменяет её полноценной (но очень хорошо скрытой и быстрой) виртуальной машиной. Затем виртуальная машина внутри себя запускает обычное ядро Linux, правильную файловую систему Linux и стандартный сетевой стек Linux. Всё это работает внутри VM.

Это означает, что красота дизайна WSL 1 исчезла: ядро Windows NT больше не видит ничего в мире Linux. Просто большой чёрный ящик, который делает что-то неизвестное внутри себя. Ядро Windows NT видит только точки подключения VMENTER и VMEXIT для виртуальной машины и запросы чтения/записи на уровне блоков на виртуальном диске. Это самое ядро NT теперь ничего не знает о процессах Linux и доступе к файлам. Точно так же работающее ядро Linux ничего не знает об NT.

О некоторых других различиях можно прочитать в официальной документации.

Потерянный потенциал


С точки зрения пользователя, подсистема WSL 2 выглядит лучше: действительно, приложения Linux теперь работают намного быстрее, потому что не проходят через неудобную эмуляцию системных вызовов Linux в ядре NT. Если NTFS трудно использовать с семантикой Linux, то теперь такой проблемы нет, потому что теперь окружение Linux использует ext4 на своём виртуальном диске. И поддержка приложений Linux может быть гораздо более полной, потому что ну, потому что WSL 2 это и есть полноценный Linux.

Но подумайте, за счёт чего достигнуто это удобство? Чего мы лишились?

Какой должна была стать WSL, если бы всё работало так, как задумано:

  • Представьте, как здорово набрать ps или top в сеансе WSL и увидеть рядом процессы Linux и Windows, причём любой из них можно убить командой kill?
  • Представьте, как здорово манипулировать службами Windows из сеанса WSL?
  • Как здорово использовать ifconfig (аналог ipconfig из Windows, хотя есть ещё ip) в рамках сеанса WSL для проверки и изменения сетевых интерфейсов компьютера?


  • По сути, можете представить выполнение абсолютно всех задач системного администрирования в Windows из линуксовой консоли WSL? Это же сказка!

Хотя такого никогда не существовало, такой мир вполне можно себе представить и это могла обеспечить только архитектура WSL 1. И это вовсе не фантазии, потому что именно такую модель использует macOS (хотя это немного читерство, ведь macOS, по сути, является Unix).

Вот что бесит сильнее всего, пишет Джулио Мерино: Хотя я могу установить WSL на свою машину разработки для Azure, но никак не могу его использовать вообще ни для чего. По-прежнему приходится работать в CMD.EXE, поскольку здесь происходит взаимодействие с нативными процессами и ресурсами Windows, а инструменты, с которыми я имею дело, предназначены только для Windows.

На странице вопросов и ответов написано, что WSL 1 не будет заброшен, то есть можно запускать дистрибутивы WSL 1 и WSL 2 вместе, проапгрейдить любой дистрибутив на WSL 2 в любое время или вернуться на WSL 1. И если посмотреть на строгую приверженность к обратной совместимости Microsoft, из-за которой мы до сих пор вынуждены пользоваться архаичными инструментами и протоколами, это может быть правдой. Но поддержка WSL 1 колоссальное усилие, потому что придётся отслеживать и соответствовать всем изменениям Linux. Как бы то ни было, будем надеяться, что WSL 1 продолжит своё существование. И кто знает, вдруг когда-нибудь всё-таки воплотится в жизнь тот волшебный мир, который мы представляем?

Уровень совместимости в BSD


Интересно, что семейство операционных систем BSD ( FreeBSD, OpenBSD, NetBSD, MidnightBSD, GhostBSD, Darwin, DragonFly BSD) всегда отставали от Linux и других коммерческих операционных систем. Но у них очень давно была реализована эта совместимость на бинарном уровне, о которой мы говорим. Джулио Мерино говорит, что совместимость с Linux в ядре NetBSD была реализована ещё в 1995 году, то есть четверть века назад и за 21 год до рождения WSL 1.

И самое замечательное, эта совместимость не ограничивается только Linux. На протяжении многих лет NetBSD поддерживала эмуляцию различных операционных систем. Поддержка SVR4 появилась в 1994 году, и в течение короткого периода времени NetBSD даже поддерживала бинарные файлы PE/COFF да, правильно, это бинарные файлы Win32! Таким образом, в некотором смысле NetBSD реализовала модель WSL 1 наоборот: она позволяла запускать бинарные файлы Win32 поверх ядра NetBSD ещё в 2002 году. Вот так вот.



На правах рекламы


VDSina предлагает VDS в аренду под любые задачи, огромный выбор операционных систем для автоматической установки, есть возможность установить любую ОС с собственного ISO, удобная панель управления собственной разработки и посуточная оплата тарифа, который вы можете создать индивидуально под свои задачи.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru