FIONREAD에 따르면 Linux SO_RCVLOWAT는 epoll에 의해 위반됩니다.

FIONREAD에 따르면 Linux SO_RCVLOWAT는 epoll에 의해 위반됩니다.

소켓을 처리하는 보다 전통적인 방법은 연결당 버퍼를 갖고 소켓을 읽을 수 있게 되면 가능한 한 많은 바이트를 점진적으로 읽는 것입니다.

TCP를 통해 실행되는 프레임워크 프로토콜의 경우 더 효율적인 접근 방식은 커널이 이미 각 연결에 대해 데이터를 버퍼링한다는 사실을 활용하는 것입니다. Linux는 필요한 바이트 수를 얻을 때까지 폴링/선택/epoll이 소켓을 읽기 가능한 것으로 표시하는 것을 방지하기 위해 SO_RCVLOWAT 플래그를 제공합니다. 이는 FIONREAD ioctl과 함께 사용하여 처리 루프에서 부분 읽기를 방지하기 위해 즉시 소비할 수 있는 바이트 수를 읽을 수 있습니다. 이 구조에서는 전체 프레임을 단일 공유 버퍼(최대 프레임 크기로 크기 조정)로 한 번에 읽어서 제자리에서 처리할 수 있으며 다음 프레임에서 덮어쓸 준비가 되어 있습니다.

그러나 구현이 중단되었습니다. Epoll은 FIONREAD에 의해 보고된 값이 SO_RCVLOWAT보다 작은 경우에도 READ 이벤트를 시작합니다. MSG_PEEK를 사용하여 recv()를 호출하면 FIONREAD와 일치하는 값이 반환되므로 소켓이 실제로아니요최소한 SO_RCVLOWAT 바이트는 즉시 읽을 수 있으므로 epoll에서 읽을 수 있는 것으로 간주해서는 안 됩니다.

다음은 문제를 재현하는 일부 샘플 서버/클라이언트 코드와 함께 구현한 것입니다.https://github.com/MrSonicMaster/broken

특히:

static void handle_reads(proto_state *s) {
  uint32_t bav;
  ioctl(s->fd, FIONREAD, &bav);

  uint32_t lowat;
  getsockopt(s->fd, SOL_SOCKET, SO_RCVLOWAT, &lowat,
             &(socklen_t){sizeof lowat});

  printf("EPOLL FIRED READ EVENT FIONREAD=%d SO_RCVLOWAT=%d\n", bav, lowat);

  if (bav < lowat) {
    /* debug code */
#define CAP (1 << 15)
    uint8_t *largebuf = alloca(CAP);
    ssize_t recvd = recv(s->fd, largebuf, CAP, MSG_PEEK);
    printf(
        "is FIONREAD lying? actual bav via recv with MSG_PEEK = %ld (cap %d)\n",
        recvd, CAP);
  }

  printf("BAV %d NEED %d\n", bav, s->hdr.len);

  msghdr hdr = s->hdr;

  while (bav >= hdr.len) {
    ssize_t read_bytes = read(s->fd, rbuffer, hdr.len);

    if (read_bytes != hdr.len) {
      fprintf(stderr, "WTF HOW? BROKE!\n");
      break;
    }

    bav -= read_bytes;

    if (s->need_hdr) {
      hdr = *(msghdr *)rbuffer;

      if (hdr.len > 16384) {
        fprintf(stderr, "msg too large %d\n", hdr.len);
        close(s->fd);
        handle_close(s);
        return;
      }

      // printf("READ HEADER, CODE %d LEN %d (udat %d)\n", hdr.code, hdr.len,
      //       hdr.udat);

      if (hdr.len == 0) {
        /* handle zero-length message */
        s->frame_cb(s, hdr, NULL);
        hdr.len = sizeof(msghdr);
        continue;
      }

      s->need_hdr = 0;
    } else {
      // printf("READ FRAME, CODE %d LEN %d\n", hdr.code, hdr.len);
      s->frame_cb(s, hdr, rbuffer);
      hdr.len = sizeof(msghdr);
      s->need_hdr = 1;
    }
  }

  s->hdr = hdr;

out_setlowat:
  if (hdr.len != s->lowat) {
    setsockopt(s->fd, SOL_SOCKET, SO_RCVLOWAT, &hdr.len, sizeof hdr.len);
    s->lowat = hdr.len;

    printf("SET lowat %d\n", s->lowat);
  }
}

...

void proto_loop() {
  int nevents = epoll_wait(ep, events, MAX_EVENTS, 0);
  if (nevents == -1) {
    perror("proto_loop() epoll_wait()");
    return;
  }

  for (int i = 0; i < nevents; i++) {
    struct epoll_event event = events[i];

    void *ptr = (void *)(((uintptr_t)event.data.ptr) & ~1);

    // printf("EVENT %d %p\n", event.events, ptr);

    if (ptr != event.data.ptr) {
      listen_desc *d = ptr;
      handle_accept(d->fd, d->cb);
      return;
    }

    if (event.events & EPOLLERR || event.events & EPOLLHUP ||
        event.events & EPOLLRDHUP)
      handle_close(ptr);
    else {
      if (event.events & EPOLLIN)
        handle_reads(ptr);
      if (event.events & EPOLLOUT)
        handle_writes(ptr);
    }
  }
}

내가 시도한 모든 구성에서 결국 다음과 같은 문제가 발생했습니다.

EPOLL FIRED READ EVENT FIONREAD=29103 SO_RCVLOWAT=14764
BAV 29103 NEED 14764
got frame with code 0 len 14764
got frame with code 0 len 5232
SET lowat 9647
EPOLL FIRED READ EVENT FIONREAD=9083 SO_RCVLOWAT=9647
is FIONREAD lying? actual bav via recv with MSG_PEEK = 9083 (cap 32768)
EPOLL FIRED READ EVENT FIONREAD=9083 SO_RCVLOWAT=9647
is FIONREAD lying? actual bav via recv with MSG_PEEK = 9083 (cap 32768)
EPOLL FIRED READ EVENT FIONREAD=9083 SO_RCVLOWAT=9647
is FIONREAD lying? actual bav via recv with MSG_PEEK = 9083 (cap 32768)
EPOLL FIRED READ EVENT FIONREAD=9083 SO_RCVLOWAT=9647
is FIONREAD lying? actual bav via recv with MSG_PEEK = 9083 (cap 32768)
...

레벨 트리거 epoll을 사용하고 버퍼를 정적 크기로 강제하여 커널 버퍼 자동 크기 조정을 비활성화합니다. 어떤 크기로 설정했는지는 중요하지 않은 것 같습니다(혼잡 창을 완전히 닫지 않고 전체 프레임에 맞을 만큼 충분히 큰 경우(예: 최대 프레임 크기의 2배 이상))

주목해야 할 또 다른 점은 서버를 종료해도 클라이언트에서 EPOLLRDHUP 이벤트가 생성되지 않는다는 것입니다.

불행히도 다른 사람들이 이 작업을 수행하는 예를 찾을 수 없는 것 같아서 이것이 효과가 있을지 모르겠습니다.

답변1

그래서 실제로 얼마 전에 이 문제를 해결했지만 답변을 게시하지 못했습니다.

비차단 Linux 소켓에 버퍼 공간이 부족해지면 SO_RCVLOWAT 설정을 위반하고 어쨌든 읽기 가능한 것으로 표시됩니다. 이 동작은 어디에도 문서화되어 있지 않습니다. 언급된 유일한 것은 Linux 커널 소스 트리에 대한 일부 커밋입니다. 문제를 보여주는 코드에서 이 문제가 발생해서는 안 되기 때문에 처음에는 이것이 사실이라고 생각하지 않았습니다. 이것은 UNIX 도메인 소켓에 대한 문서화되지 않은 추가 최적화로 인해 발생하는 것으로 생각됩니다. 이로 인해 송신자 SNDBUF가 수신자 RCVBUF로 전송됩니다. 일괄 전송을 한 번에 넣을 수 있는 것보다 더 큰 청크로 보내는 방법입니다(단일 전송보다 커야 함). SO_RCVLOWAT를 위반할 때 SO_ERROR로 getsockopt()를 호출하면 ENOBUFS가 반환되기 때문에 이것이 문제라는 것을 확인할 수 있습니다.

UNIX 도메인 소켓에서 이 작업을 수행하는 솔루션은 수신자 RCVBUF가 전체 발신자 SNDBUF보다 더 많은 것을 보유할 만큼 충분히 큰지 확인하여 Linux 내에서 수행된 최적화/일괄 처리로 인해 프로그램 논리가 중단되지 않도록 하는 것입니다.

관련 정보