Это первый день моего участия в августовском испытании обновлений, подробности о мероприятии:Испытание августовского обновления
предисловие
Книга продолжает вышесказанное, java-код и системные вызовы имеют определенную связь, Java - это интерпретируемый язык (Java не является ценным, ценным является jvm), написанный нами java-код, наконец, компилируется в байт-код, а затем переходит к system Call, в этой статье мы еще научимся понимать io из простой серверной программы.
BIO
Независимо от того, какой это язык, если это серверная программа, она должна выполнять следующие операции:
-
Вызовите сокет, чтобы получить описание файла (символ представляет этот сокет)
-
привязать порт привязки, например 8090
-
слушать слушать статус
-
accept принимает клиентские соединения
Продолжаем использовать тестовую демку из предыдущей статьи и подключаемся к серверу через клиент.На картинке ниже видно, что основной поток находится в состоянии опроса (мультиплексирование будет введено позже) и заблокирован в этом месте.
public class ServerSocketTest {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8090);
System.out.println("step1:new ServerSocket(8090)");
while (true){
Socket client = server.accept();
System.out.println("step2:client\t"+client.getPort());
new Thread(() ->{
try {
InputStream in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true){
System.out.println(reader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
Потом подключаемся к серверу через клиент и смотрим на него видно что он только что заблокировался (выше) и сейчас подключается событие, а после его прихода запускается accept, и новый файловый дескриптор 6 (это клиент)
Спустившись ниже, я нашел клона, что это за штука? Вы, должно быть, догадались, что это наш новый Thread(() выше, каждый раз, когда клиент получает, создается новый поток, идентификатор потока: 16872
Мы можем проверить, есть ли лишний поток 16872, как показано на рисунке ниже, есть лишний поток 16872
Смотрим на только что построенный поток и обнаруживаем, что этот поток заблокирован в Recvfrom(6, это совмещается с теорией, и данные клиента не пришли. Поток заблокирован.
Далее даем ввод на клиенте, а потом смотрим ответ сервера
Выяснено, что контент читается из дескриптора 6, затем пишется1 (ранее мы говорили, что 1 — это стандартный вывод), и мы видим контент, вводимый клиентом на стороне сервера
Здесь отчетливо виден смысл блокировки io.Приведенный выше пример — обработка мультиклиентских запросов через многопоточность (каждому клиенту соответствует поток), и в этом процессе должно быть участие ядра (независимо от Независимо от того, принимает ли функция, которая поставляется с jvm или lib, вызов sys)
Вышеописанная операция, давайте опишем ее небольшой картинкой
На самом деле это БИО, типичное БИО, и в чем здесь проблема?
Потоков слишком много, а потоки тоже ресурсы.Создание потока потребует системного вызова (клон, описанный выше), поэтому неизбежно произойдет мягкое прерывание; мы знаем, что память стека потоков независима, поэтому это вызовет потребление ресурсов памяти и переключение ЦП.Тоже будут траты.На самом деле это все поверхностные проблемы.Фундаментальная проблема на самом деле блокировка IO
Итак, как сделать его неблокирующим?Давайте посмотрим на описание системного вызова сокета через команду man socket, на следующем рисунке показано, что сокет можно сделать неблокирующим в ядре (socket
Ядро Linux 2.6.27 начало поддерживать неблокирующий режим. )
NIO
Добавление параметра к вызову может сделать сокет неблокирующим.В это время считываемый fd не блокируется.Если есть данные, они будут считаны напрямую, а если данных нет, они будут возвращены напрямую. Это так называемый NIO.На самом деле существуют следующие два утверждения о NIO.
- С точки зрения App lib N означает новый, то есть новую систему ввода-вывода с каналами, буферами... этими новыми компонентами.
- С точки зрения ядра операционной системы N означает отсутствие блокировки и неблокировку.
По сравнению с предыдущим BIO, эта модель решает проблему открытия многих потоков, она кажется более мощной, чем предыдущая, так в чем проблема с этим?
Я не знаю, слышали ли вы о проблеме C10K.Например, если есть клиенты 1w, то ядро системы будет вызываться в цикле 1w раз каждый раз, чтобы посмотреть, есть ли какие-либо данные, а это означает, что каждый цикл будет иметь сложность O (n) Процесс системного вызова, но, возможно, только несколько из 1w раз имеют данные или готовы, то есть большинство системных вызовов заняты, что является пустой тратой ресурсов!
Как решить эту проблему? Можно ли снизить сложность O (n), зависит от того, как оптимизировано ядро. Мы можем посмотреть на команду select. Следующее ее описание: "Позволяет программе прослушивать несколько файловых дескрипторов, ожидая, пока один или несколько файловых дескрипторов станут доступными."
SYNOPSIS
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
--
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
pselect(): _POSIX_C_SOURCE >= 200112L
DESCRIPTION
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input pos-
sible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2) without blocking, or a sufficiently small write(2)).
select() can monitor only file descriptors numbers that are less than FD_SETSIZE; poll(2) does not have this limitation. See BUGS.
The operation of select() and pselect() is identical, other than these three differences:
(i) select() uses a timeout that is a struct timeval (with seconds and microseconds), while pselect() uses a struct timespec (with seconds and nanoseconds).
(ii) select() may update the timeout argument to indicate how much time was left. pselect() does not change this argument.
Это легко понять по следующему рисунку.Программа сначала вызывает системный вызов с именем select, а затем передает в fds (если есть файловые дескрипторы 1w, отправьте эти файловые дескрипторы 1w ядру в этом системном вызове. ), ядро вернет несколько файловых дескрипторов доступных состояний, и окончательные данные чтения основаны на этих нескольких файловых дескрипторах доступных состояний для доступа к ядру для чтения.Эта сложность системы может быть понята как O (m), которому предшествует nio (каждый клиент A спрашивает, готово ли), теперь все клиентские соединения кидаются в инструментальный выбор (мультиплексор); поэтому многие клиентские соединения мультиплексируют системный вызов, возвращая готовые соединения, а дальше программа сама делает чтение и запись
Эта модель мультиплексирования, по сравнению с вышеописанной сложностью системы nio o(n), мультиплексирование уменьшает количество системных вызовов до o(m), но при этом все равно необходимо выполнять ядро за O(n) активного обхода, поэтому следующие две проблемы
- select должен передавать значения каждый раз (10w fds)
- Ядро активно просматривает, какие fds доступны для чтения и записи.
Есть ли способ решить эту проблему? Да, должен быть. Давайте продолжим смотреть на это...
мультиплексирование epoll
Если в ядре может быть открыто место, каждый раз, когда программа получает соединение, она будет сохранять дескриптор файла соединения в быстром ядре, поэтому ей не нужно повторять передачу в следующий раз, сокращая процесс передачи, тогда как узнать, какие fds доступны. Что насчет чтения/записи? Предыдущий метод был активным обходом, если их 1w то он будет пройден 1w раз.Как оптимизировать этот метод активного обхода, чтобы он работал быстрее, на самом деле есть метод, управляемый событиями, и на этом нужен epoll время. Вы можете просмотреть документацию epoll с помощью команды man epoll
The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them. The epoll API can be used either as an edge-triggered or a
level-triggered interface and scales well to large numbers of watched file descriptors. The following system calls are provided to create and manage an epoll instance:
* epoll_create(2) creates a new epoll instance and returns a file descriptor referring to that instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)
* Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.
* epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.
Видно, что epoll в основном имеет три команды epoll_create, epoll_ctl, epoll_wait
epoll_create
epoll_create: ядро сгенерирует структуру данных экземпляра epoll и вернет файловый дескриптор epfd, который на самом деле описывает быстрое пространство, открытое в описанном выше ядре.
DESCRIPTION
epoll_create() creates a new epoll(7) instance. Since Linux 2.6.8, the size argument is ignored, but must be greater than zero; see NOTES below.
epoll_create() returns a file descriptor referring to the new epoll instance. This file descriptor is used for all the subsequent calls to the epoll interface. When no longer required, the
file descriptor returned by epoll_create() should be closed by using close(2). When all file descriptors referring to an epoll instance have been closed, the kernel destroys the instance and
releases the associated resources for reuse.
epoll_create1()
If flags is 0, then, other than the fact that the obsolete size argument is dropped, epoll_create1() is the same as epoll_create(). The following value can be included in flags to obtain
different behavior:
EPOLL_CLOEXEC
Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.
RETURN VALUE
On success, these system calls return a nonnegative file descriptor. On error, -1 is returned, and errno is set to indicate the error.
epoll_ctl
Зарегистрируйте, удалите или измените дескриптор файла fd и его событие прослушивания epoll_event.
DESCRIPTION
This system call performs control operations on the epoll(7) instance referred to by the file descriptor epfd. It requests that the operation op be performed for the target file descriptor,
fd.
Valid values for the op argument are:
EPOLL_CTL_ADD
Register the target file descriptor fd on the epoll instance referred to by the file descriptor epfd and associate the event event with the internal file linked to fd.
EPOLL_CTL_MOD
Change the event event associated with the target file descriptor fd.
EPOLL_CTL_DEL
Remove (deregister) the target file descriptor fd from the epoll instance referred to by epfd. The event is ignored and can be NULL (but see BUGS below).
epoll_wait
Блокирует ожидание возникновения зарегистрированных событий, возвращает количество событий и записывает сработавшие доступные события в массив epoll_events.
SYNOPSIS
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
DESCRIPTION
The epoll_wait() system call waits for events on the epoll(7) instance referred to by the file descriptor epfd. The memory area pointed to by events will contain the events that will be
available for the caller. Up to maxevents are returned by epoll_wait(). The maxevents argument must be greater than zero.
The timeout argument specifies the number of milliseconds that epoll_wait() will block. Time is measured against the CLOCK_MONOTONIC clock. The call will block until either:
* a file descriptor delivers an event;
* the call is interrupted by a signal handler; or
* the timeout expires.
Весь процесс запуска программы
socket fd5 创建socket等到fd5
bind 8090 绑定8090
listen fd5 监听fd5
epoll_create fd8 在内核中创建一块区域 fd8
epoll_ctl(fd8,add fd5,accept) 在fd8这个区域中把fd5放进去
epoll_wait(fd8) 让程序等待文件描述符达到就绪状态
accept fd5 -> fd6 此时新进来一个文件描述符为fd6的客户端
epoll_ctl(fd8,fd6) 把fd6放到fd8的内核空间中去,如果有很多客户端,fd8内核区域中会有很多的fd
Как реализовать управление событиями, вам нужно подробно представить концепцию прерывания, вот краткое введение (надеюсь сделать статью, чтобы представить его отдельно): Например, когда клиент отправляет данные на сетевую карту сервер, после того, как сетевая карта получит данные, произойдет прерывание процессора, процессор перезвонит, и ядро узнает, какой фд находится через DMA (прямой доступ к памяти), а затем перенесет готовый фд из области а в b на рисунке; клиент напрямую получает готовый fd для чтения Write
Здесь можно немного подытожить, epoll фактически дает полную свободу аппаратному обеспечению и не тратит ресурсы процессора, мультиплексирование из bio->nio->epoll предназначено для решения проблем, существующих в существующей модели, и создания новой модели. относится не только ко всем вещам в области технологий, я напишу здесь сегодня и надеюсь, что буду продолжать усердно работать в будущем!
В узком переулке, где встречаются солдаты, убивают, как траву, не слыша ни слова - Шэнь Минчэнь