Пять моделей ввода-вывода и анализ исходного кода Java NIO

Java
Пять моделей ввода-вывода и анализ исходного кода Java NIO

 Недавно изучая сетевое программирование на Java и знания, связанные с Netty, я узнал, что Netty — это сетевая структура режима NIO, но предоставляет различныеChannelДля поддержки различных режимов обработки сетевых коммуникаций, включая синхронный, асинхронный, блокирующий и неблокирующий. Обучение начинается с основ, поэтому мы должны сначала понять соответствующие базовые концепции и собственный NIO Java. Здесь я подытожу то, что я недавно узнал для вашего понимания.

  В целях экономии вашего времени основное содержание этой статьи выглядит следующим образом:

  • Асинхронная, блокирующая концепция
  • Тип ввода/вывода ОС
  • Базовая реализация Java NIO

Асинхронный, синхронный, блокирующий, неблокирующий

Синхронный и асинхронный фокус на механизмах передачи сообщений, так называемая синхронизация означает, что после того, как вызывающая сторона сделает вызов, вызов не вернется до тех пор, пока не будет получен результат, но как только вызов вернется, будет получено возвращаемое значение.Синхронизация означает, что вызывающая сторона активно ожидает результата вызова.;Наоборот, асинхронно, он возвращается сразу после выполнения вызова, поэтому возвращаемого значения может не быть.Когда есть возвращаемое значение, вызываемый уведомит вызывающего через состояние и уведомление.Асинхронный означает, что вызываемый объект уведомляет вызывающего о том, что результат вызова готов.*. *Следовательно, они имеют разные механизмы передачи сообщений: один для того, чтобы вызывающая сторона проверяла, готов ли результат вызова, а другая — для того, чтобы вызываемая сторона уведомляла вызывающую сторону о том, что результат готов.

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

 Существуют четыре ситуации, когда два набора понятий объединяются друг с другом, а именно: синхронная блокировка, синхронная неблокировка, асинхронная блокировка и асинхронная неблокировка. Давайте возьмем пример для сравнения четырех случаев апелляции по отдельности.

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

Типы ввода/вывода Unix

  Зная приведенные выше два набора концепций, давайте рассмотрим 5 моделей ввода-вывода, доступных в Unix:

  • Блокирующий ввод-вывод (блокирующий ввод-вывод)
  • Неблокирующий ввод-вывод (неблокирующий ввод-вывод)
  • Мультиплексирование ввода-вывода (мультиплексирование ввода-вывода)
  • Ввод-вывод, управляемый сигналом (ввод-вывод, управляемый сигналом)
  • Асинхронный ввод-вывод (асинхронный ввод-вывод)

Первые четыре являются синхронными, только последний является асинхронным вводом-выводом.Следует отметить, что ***Java NIO полагается на мультиплексирующий ввод-вывод системы Unix.Для операций ввода-вывода это синхронный ввод-вывод , но для модели программирования это асинхронный сетевой вызов*** Ниже мы будем использовать системуreadвызов, чтобы представить различные типы ввода/вывода.

  бытьreadКогда это происходит, оно проходит две стадии:

  • 1 Дождитесь подготовки данных
  • 2 Скопируйте данные из памяти ядра в память процесса

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

阻塞I/O

非阻塞I/O

多路复用I/O

信号驱动

异步I/O

Базовая реализация Java NIO

 Все мы знаем, что Netty предоставляет Native Socket Transport через JNI, почемуNettyКак насчет предоставления собственной версии NIO? Очевидно, что нижний уровень Java NIO также основан наepollЗвоните (последняя версия). Здесь не будем сначала об этом, давайте подумаем о возможных ситуациях. Следующий исходный код взят из версии OpenJDK-8u40-b25.

открытый метод

  Если мы будем следоватьSelector.open()Легко найти метод по классу за классомSelectorинициализируетсяDefaultSelectorProviderгенерируются в соответствии с различными платформами операционных системSelectorProvider, для систем Linux генерируетEPollSelectorProviderэкземпляр, и этот экземпляр будет генерироватьEPollSelectorImplкак финалSelectorвыполнить.

class EPollSelectorImpl extends SelectorImpl
{
    .....
    // The poll object
    EPollArrayWrapper pollWrapper;
    .....
    EPollSelectorImpl(SelectorProvider sp) throws IOException {
        .....
        pollWrapper = new EPollArrayWrapper();
        pollWrapper.initInterrupt(fd0, fd1);
        .....
    }
    .....
}

EpollArrayWapperСистемные вызовы Linux, связанные с epoll, инкапсулированы в собственные методы дляEpollSelectorImplиспользовать.

    private native int epollCreate();
    private native void epollCtl(int epfd, int opcode, int fd, int events);
    private native int epollWait(long pollAddress, int numfds, long timeout,
                                 int epfd) throws IOException;

 Приведенные выше три нативных метода соответствуют трем системным вызовам, связанным с epoll в Linux.

//创建一个epoll句柄,size是这个监听的数目的最大值.
int epoll_create(int size);
//事件注册函数,告诉内核epoll监听什么类型的事件,参数是感兴趣的事件类型,回调和监听的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件的产生,类似于select调用,events参数用来从内核得到事件的集合
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

  Итак, мы найдем вEpollArrayWapperвызывается в конструктореepollCreateметод, который создает дескриптор для epoll. так,SelectorДаже если объект создан.

метод регистрации

иopenаналогичный,ServerSocketChannelизregisterНижний слой функции называетсяSelectorImplКатегорияregisterметод, этоSelectorImplто естьEPollSelectorImplродительский класс.

protected final SelectionKey register(AbstractSelectableChannel ch,
                                      int ops,
                                      Object attachment)
{
    if (!(ch instanceof SelChImpl))
        throw new IllegalSelectorException();
    //生成SelectorKey来存储到hashmap中,一共之后获取
    SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
    //attach用户想要存储的对象
    k.attach(attachment);
    //调用子类的implRegister方法
    synchronized (publicKeys) {
        implRegister(k);
    }
    //设置关注的option
    k.interestOps(ops);
    return k;
}

EpollSelectorImplСоответствующий метод реализован следующим образом, который вызываетEPollArrayWrapperизaddметод, запишите значение fd, соответствующее каналу, а затем добавьте лыжу вkeysв переменной. существуетEPollArrayWrapperЕсть массив байтовeventLowЗапишите значения fd всех каналов.

    protected void implRegister(SelectionKeyImpl ski) {
        if (closed)
            throw new ClosedSelectorException();
        SelChImpl ch = ski.channel;
        //获取Channel所对应的fd,因为在linux下socket会被当作一个文件,也会有fd
        int fd = Integer.valueOf(ch.getFDVal());
        fdToKey.put(fd, ski);
        //调用pollWrapper的add方法,将channel的fd添加到监控列表中
        pollWrapper.add(fd);
        //保存到HashSet中,keys是SelectorImpl的成员变量
        keys.add(ski);
    }

  Мы найдем это призваниеregisterметод не предполагаетEpollArrayWrapperродной метод вepollCtl, потому что они откладывают вызов этого метода до тех пор, покаSelectв методе.

Выберите метод

иregisterметод аналогичен,SelectorImplсерединаselectметод заканчивает тем, что вызывает свой подклассEpollSelectorImplизdoSelectметод

protected int doSelect(long timeout) throws IOException {
    .....
    try {
        ....
        //调用了poll方法,底层调用了native的epollCtl和epollWait方法
        pollWrapper.poll(timeout);
    } finally {
        ....
    }
    ....
    //更新selectedKeys,为之后的selectedKeys函数做准备
    int numKeysUpdated = updateSelectedKeys();
    ....
    return numKeysUpdated;
}

  Из приведенного выше кода видно, чтоEPollSelectorImplсначала позвониEPollArrayWapperизpollметод, затем обновитеSelectedKeys. вpollметод будет вызываться первымepollCtlпредварительно зарегистрироваться наregisterFd канала, сохраненный в методе и интересующем типе события, затемepollWaitМетод ожидает генерации интересующего события, что приводит к блокировке потока.

int poll(long timeout) throws IOException {
    updateRegistrations(); ////先调用epollCtl,更新关注的事件类型
    ////导致阻塞,等待事件产生
    updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
    .....
    return updated;
}

  После ожидания наступления интересующего события (или после того, как время ожидания превысит заданное максимальное время),epollWaitФункция вернется.selectФункция возобновляет работу из заблокированного состояния.

метод selectedKeys

 Давайте сначала посмотримSelectorImplсерединаselectedKeysметод.

//是通过Util.ungrowableSet生成的,不能添加,只能减少
private Set<SelectionKey> publicSelectedKeys;
public Set<SelectionKey> selectedKeys() {
    ....
    return publicSelectedKeys;
}

 Странно, зачем сразу возвращатьсяpublicSelectedKeys, этоselectБыла ли эта переменная изменена во время выполнения функции?

publicSelectedKeysЭтот объект на самом делеselectedKeysКопия переменной, вы можете найти ее вSelectorImplНайдите связь между ними в конструкторе , давайте оглянемся назадselectсерединаupdateSelectedKeysметод.

private int updateSelectedKeys() {
    //更新了的keys的个数,或在说是产生的事件的个数
    int entries = pollWrapper.updated; 
    int numKeysUpdated = 0;
    for (int i=0; i<entries; i++) {
        //对应的channel的fd
        int nextFD = pollWrapper.getDescriptor(i);
        //通过fd找到对应的SelectionKey
        SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
        if (ski != null) {
            int rOps = pollWrapper.getEventOps(i);
            //更新selectedKey变量,并通知响应的channel来做响应的处理
            if (selectedKeys.contains(ski)) {
                if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                    numKeysUpdated++;
                }
            } else {
                ski.channel.translateAndSetReadyOps(rOps, ski);
                if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                    selectedKeys.add(ski);
                    numKeysUpdated++;
                }
            }
        }
    }
    return numKeysUpdated;
}

постскриптум

 Увидев это, все уже подробно разобрались в базовой реализации NIO.Здесь я хочу поговорить о двух вопросах.

 Во-первых, почему Netty повторно реализовала базовые методы NIO, связанные с нативной стороной? Послушайте, что должен сказать основатель Netty.Ссылка на сайт. Поскольку версия Java использует режим epoll, запускаемый уровнем, в то время как Netty хочет использовать режим, запускаемый границей, а версия Java не предоставляет некоторые элементы конфигурации epoll, такие как TCP_CORK и SO_REUSEPORT.

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

  Если в будущем будет время, я надеюсь хорошо понять принцип реализации epoll на уровне операционной системы.