BigAdmin System Administration Portal
특집 기사: Solaris OS 네트워킹 -- 마술의 비밀 공개
Print-friendly VersionPrint-friendly Version

Solaris OS 네트워킹 -- 마술의 비밀 공개

Sunay Tripathi(수석 엔지니어, Solaris Core Technology 그룹), 2006년 1월

요약: 이 기사에서는 Solaris 10 OS에서의 네트워크의 진보뿐만 아니라, 구 릴리스에서의 네트워크의 진화에 대해서도 설명합니다. TCP, UDP, IP, 장치 드라이버 프레임워크, 성능 조정을 위한 조정 등을 주제로 다루고 있습니다.

목차


1.0 배경

Solaris 1.x 운영 체제의 네트워크 스택은 BSD 형식으로 BSD Reno를 설치했을 때와 유사합니다. BSD 스택은 저사양 시스템에는 적합했지만, 저사양을 원하는 고객 뿐만이 아니라 기업 고객의 요구도 만족 시키기 위해 Solaris OS를 AT&T SVR4 구조로 변경한 것이 바로 Solaris 2.x 플랫폼입니다.

Solaris 2.x OS로 업그레이드되면서 네트워크 스택은 BSD 스타일의 스택에서 STREAMS를 기반으로 하는 스택으로 변경되었습니다. STREAMS 프레임워크는 간단한 메시지를 주고받는 인터페이스를 갖추고 있으므로 STREAMS 모듈 사이에서 유연한 메시지 교환이 가능했습니다. STREAMS의 내부 및 외부 경계를 사용하여 모듈의 개발자의 입장에서는 내부 구성을 복잡하지 않으며 상호 배타 실현을 가능케 했습니다. STREAMS 하나를 설정하는 비용이 높긴 했지만, 대개 접속 유지 시간이 길기 때문에 초당 몇 개의 접속을 설정했는지는 그리 중요하지 않습니다. NFS나 FPT 등과 같이 장기간을 두고 보면 접속 시간이 길어질수록 새로운 STREAMS의 설정 비용은 상환될 것입니다.

1990년대 후반에 들어서면서 서버는 다수의 CPU를 실행하는 SMP 기반을 갖추기 시작했습니다. 중급 사양 시스템에서 고급 사양 시스템이 NUMA의 주류를 이루기 시작하면서 CPU 전환 처리 비용이 증가하였습니다.STREAMS는 설계상 CPU와 밀접하게 관계가 없기 때문에 특정 접속의 패킷이 다양한 CPU 사이를 이동합니다. Solaris 제품이 STREAMS 구조를 탈피할 필요가 있는 것은 명백했습니다.

1990년대 후반은 World Wide Web이 폭발적으로 확대되었던 시기였습니다. 처리 능력이 향상됨에 따라 단시간 접속이 폭주하였으며, 접속 설정 시간도 그만큼 중요하게 되었습니다. Solaris 10 플랫폼에서는 네트워크 스택이 한층 더 변화하였으며, 코어 부분(소켓 층, TCP, UPD, IP, 장치 드라이버 등)에서 IP Classifier와 직렬화된 큐를 사용함으로써 접속 설정 시간, 확장성 및 패킷 처리 능력을 향상시켰습니다. ISV가 추가 기능을 갖추기 위해 필요한 유연성을 제공하기 위해서 STREAMS 구조는 계속 사용되고 있습니다.


2.0 Solaris 10 OS의 스택

새로운 프레임워크와 그 주요 부분들의 동작 원리에 대해서 살펴봅시다.

2.1 요약

Solaris 10 OS 이전의 스택은 STREAMS 경계와 커널 적응형의 상호 배타적인 성질을 이용해 멀티 스레딩에 대처하고 있습니다. TCP는 STREAMS QPAIR 경계를 사용하고, UDP는 STREAMS QPAIR 와 PUTSHARED를 사용하며, IP는 PERMOD 경계와 PUTSHARED를 사용합니다. 또, 상호 배타로 보호되는 각종의 TCP, UDP 및 IP 전역 데이터 구조가 사용됩니다. 스택은 다양한 시스템 호출, 네트워크 장치 드라이버의 읽기 인터럽트 또는 장치 드라이버의 work-thread를 실행하는 사용자측의 thread와 STREAMS 프레임워크의 work-thread 양쪽 모두에 의해서 실행됩니다. 현재의 경계는 모듈 단위, 프로토콜 스택 레이어 단위 또는 수평 경계를 제공합니다. Solaris 9 이하 릴리스의 STREAMS 기반 스택에서는 각각의 프로토콜 레이어에 대해서 수평 경계가 제공되기 때문에 패킷이 여러 개의 CPU 상에서 프로토콜 레이어 사이에서 큐잉되는 여러 개의 스레드에 의해서 처리되는 경우가 있었습니다. 그 결과 컨텍스트가 빈번히 변환되어 접속 고유의 데이터 구조에 대해서 데이터의 국소성이 저하되었습니다.

"FireEngine"방식은 모든 프로토콜 레이어를 완전하게 멀티 스레딩된 하나의 STREAMS 모듈로 병합합니다. 병합된 모듈 내에서는 데이터 구조 단위의 잠금이 아니라 "수직 경계"라고 불리는 CPU 단위의 동기화 메커니즘이 사용됩니다. "수직 경계"는 "squeue"라고 불리는 직렬화된 큐 추상화를 사용해 마운트됩니다. 각 squeue는 CPU에 바인드되어 각 접속은 접속 고유의 데이터 구조에서 필요한 동기화와 상호 배타를 제공하는 squeue에 바인드됩니다.

인바운드 패킷의 접속(또는 컨텍스트) 조회는 패킷이 IP에 도달하는 것과 동시에 경계의 외측에서 IP 접속 분류자를 사용하여 이루어집니다. 등급설정에 근거하여 접속 구조가 식별됩니다. 조회는 경계의 외부에서 발생하기 때문에 접속을 초기화할 때 수직 경계 또는 "squeue"의 임시 접속을 바인드하여, 대상 바인드의 squeue로 해당 접속의 모든 패킷을 처리하는 것으로 캐시의 국소성이 향상됩니다. 수직 경계와 분류자의 자세한 내용은 다른 섹션에서 설명합니다. 분류자는 모든 인바운드 및 아웃바운드 패킷에 필요한 일련의 함수 호출을 저장하는 데이터베이스의 역할을 하기도 합니다. 이로써 Solaris 네트워크 스택은 현재 메시지를 주고받는 인터페이스에서 BSD 스타일의 함수 호출 인터페이스로 바뀝니다. 접속 패킷 처리용 온더 플라이로 작성되는 함수의 문자열(이벤트 리스트)이 최종적인 새로운 프레임워크의 기초이며, 다른 모듈이나 Sun 이외의 고성능 모듈이 이 프레임워크에 참가할 수 있습니다.

2.2 수직 경계

squeue에 의해 항상 하나의 스레드만이 특정 접속을 처리하기 위해 통합된 TCP/IP 모듈 내의 여러 스레드(읽기 측과 쓰기 측)에 의한 TCP 접속 구조에 대한 액세스가 직렬화됩니다. 이는 STREAMS QPAIR 경계와 유사하지만, 모듈 인스턴스를 보호할 뿐만 아니라 IP에서 sockfs까지의 접속 상태 전체가 보호됩니다.

수직 경계나 squeue 자체는 패킷의 직렬화와 데이터 구조의 상호 배타를 제공할 뿐이지만, CPU 단위의 경계를 작성하여 CPU 처리 중에 접속된 인스턴스에 대한 접속을 바인드함으로써 데이터의 국소성을 높일 수 있습니다.

접속 단위의 경계 또는 CPU 단위의 경계라는 선택사항이 있습니다. 즉, 접속별 인스턴스 또는 CPU별 인스턴스라는 선택사항입니다. 접속 단위의 경계에 수반하는 오버헤드와 스레드의 경합에 의해 성능이 저하되는 것을 고려하고, CPU 단위의 인스턴스가 채용되었습니다. CPU 단위의 인스턴스인 경우에는 접속구조를 큐잉해서 처리하는 방법 또는 패킷 자체를 큐잉하는 대신 접속 구조의 포인터를 패킷에 저장하는 방법이 고려되었습니다. 전자의 방법일 경우 접속용 패킷이 끊임없이 도착하는 상황에서는 모든 패킷을 처리하지 못할 우려가 있습니다. 이러한 상황을 회피하기 위한 오버헤드로 인해 성능이 감소했습니다. 패킷의 큐잉에서는 순서가 지켜지며 처리도 매우 간단하기 때문에 FireEngine에서는 이 방법이 채용되었습니다.

전술한 바와 같이 각 접속 인스턴스는 한 개의 squeue에 할당되어 수직 경계 안에서만 처리됩니다. squeue는 한 번에 1개의 스레드에 의해 처리되기 때문에 경계 내에서 특정 접속을 처리하는데 사용되는 모든 데이터 구조는 잠겨있지 않으므로 쉽게 액세스할 수 있습니다. 이 결과 접속 메타데이터, 패킷 메타데이터 및 패킷 유효 부하 데이터의 접속, CPU와 스레드 컨텍스트 데이터의 국소성이 향상됩니다. 게다가 장치 단위의 드라이버 워커 스레드 방식을 사용하지 않아도 됩니다. 이 방식으로는 시스템 전체의 리소스 문제를 해결할 수는 없습니다. 또한 새로운 방침의 알고리즘을 채택하여 네트워크 인터페이스의 작업 처리량 및 시스템 작업 처리량에 근거하여 특정 네트워크 인터페이스를 적절히 처리할 수 있습니다(예를 들어 접속 단위의 패킷 처리를 CPU의 그룹으로 분산합니다). squeue에 들어가는 스레드가 패킷을 곧바로 처리하는 경우도 있습니다. 혹은 그것을 큐잉하고 나중에 다른 스레드 또는 워커 스레드로 처리하는 경우도 있습니다. 그 선택은 squeue의 진입점과 직렬화의 상태에 따라 달라집니다. 즉시 처리는 같은 squeue에 다른 스레드가 들어가 있지 않은 경우에게만 가능합니다. squeue는 다음의 추상화로 나타낼 수 있습니다.

typedef  struct squeue_s {
    int_t     sq_flag;     /* Flags tells squeue status */
    kmutex_t sq_lock;      /* Lock to protect the flag etc */
    mblk_t     *sq_first;  /* First Packet */
    mblk_t     *sq_last;   /* Last Packet */
    thread_t sq_worker;    /* the worker thread for squeue */
} squeue_t;

squeue는 코어나 하이퍼 스레드 등 H/W 실행 파이프 라인을 기준으로 작성된다는 점에 주의하십시오. 직렬화 큐(및 H/W 실행 파이프 라인)의 스택 처리는 한 번에 1개의 스레드로 한정되지만, 새로운 스택이 메모리나 수직 경계 내의 잠금 등의 리소스가 없어도 되기 때문에 실제로 성능이 개선됩니다. 또한 여러 개의 커널 스레드가 H/W 실행 파이프 라인을 시간 공유하는 것이 한 개의 스레드만이 인터럽트를 당하지 않고 실행되는 것보다 오버헤드를 수반합니다.

  • 큐잉 모델: 큐는 읽기 측과 쓰기 측 모두에 대해 엄밀하게 선입선출(First In First Out) 방식이므로 특정 접속이 처리되지 않는 경우는 없습니다. 읽기 측 또는 쓰기 측의 스레드는 큐의 말미에 패킷을 넣습니다. 그리고 나서 아래의 처리 모델에 근거하여 패킷을 처리하거나 워커 스레드에 통지합니다.
  • 처리 모델: 패킷을 큐에 넣은 다음 다른 스레드가 벌써 squeue를 처리하고 있는 경우는 큐잉 스레드가 돌아와 패킷은 드레인 모델에 근거하여 나중에 드레인됩니다. squeue가 처리되지 않고 한편, 패킷이 큐잉되어 있지 않은 경우에 스레드는 squeue에 처리 중이라는 마크를 붙이고(sq_flag로 표현), 패킷을 처리할 수 있습니다. 패킷 처리가 완료되면 "처리 중"이라는 플래그가 삭제되어 squeue는 사용 가능한 상태가 됩니다.
  • 드레인 모델: 패킷을 적절히 처리한 스레드는 요청을 처리 중에 큐에 들어간 패킷을 드레인할 수도 있습니다. 나아가 squeue는 처리되어 있지 않지만 벌써 큐잉 되고 있는 패킷이 있는 경우에 패킷을 큐잉하고 돌아오는 것이 아니라, 큐를 드레인하여 자체 패킷을 처리할 수 있습니다.

워커 스레드는 항상 큐 전체를 드레인할 수 있습니다. 적절한 드레인 모델을 선택하는 것은 매우 어렵지만 아래에서 몇 가지 예를 들어 보겠습니다.

  • "항상 큐잉한다"
  • "가능하면 자체 패킷을 처리한다"
  • "기한부의 처리와 드레인"

이러한 옵션은 읽기 스레드와 쓰기 스레드에 개별적으로 적용할 수 있습니다.

통상 인터럽트 스레드에 의한 드레인은 항상 기한부 "드레인과 처리"인데 반하여 쓰기 스레드는 "자체 패킷을 처리 "또는 기한부 "처리와 드레인" 중 하나입니다. Solaris 10 릴리스에서는 쓰기 스레드의 동작은 튜닝 가능한 변수로 지정되어 기본값은 "자체 패킷을 처리"이지만, 읽기 측은 "기한부 처리와 드레인"으로 고정되어 있습니다.

이와는 별도로 워커 스레드의 통지는 반드시 검토해야 하는 중요한 옵션입니다. 패킷의 도착율이 낮고 스레드가 자체 패킷을 큐잉하도록 설정되어 있는 경우 실행할 작업이 있으면 입력 스레드가 squeue의 처리 종료와 더불어 동시에 워커 스레드의 실행을 허가해야 합니다.

한편 패킷의 도착율이 높은 경우는 워커 스레드 기동을 늦추고, 드레인을 완료한 후 곧바로 패킷이 도착하여 인터럽트가 발생하는 것을 기대하는 편이 바람직할 경우가 있습니다. 패킷의 도착율이 높은 상태에서 워커 스레드를 곧바로 기동하면 워커 스레드와 인터럽트 스레드 사이에 불필요한 경합이 생깁니다.

Solaris 10 OS에서는 워커 스레드가 늦게 기동하는 것이 기본값으로 설정되어 있습니다. 이용 가능한 서버에서 최초로 시험해 본 결과 10 분 늦게 워커 스레드를 기동했을 때에 최적의 결과를 얻을 수 있었습니다.

squeue에 요청을 넣으려면 squeue 단위의 잠금으로 큐 상태를 보호할 필요가 있지만, CPU에 분산되고 있어 그 시간도 짧기 때문에 확장성 문제는 발생하지 않습니다. 최적화도 이용하고 있고 이로써 squeue 처리에 있어서의 단일 스레드의 의미를 유지한 채로 컨텍스트의 전환을 회피할 수 있습니다. 시스템 안의 CPU별로 squeue의 인스턴스를 작성하여 해당 CPU에 워커 스레드를 바인드합니다. 그리고 각 접속은 특정 squeue 나아가 특정 CPU에 바인드됩니다.

squeue와 CPU의 바인드는 변경할 수 있지만 squeue를 보호하는 의미에서 접속과 squeue의 바인드를 변경할 수 없습니다. 통합된 TCP/IP의 경우 수직 경계가 각 접속의 TCP 상태를 보호합니다. 각 접속이 사용하는 squeue 인스턴스는 아웃바운드 접속의 경우 "오픈", "바인드" 또는 "접속" 시간에 선택되고, 인바인드 접속의 경우 "접속 요청 생성 시간"에 선택됩니다.

squeue 인스턴스의 선택은 시스템 안의 CPU와 NIC의 상대속도로 결정됩니다. 2가지 경우가 있습니다.

  • CPU가 NIC보다 고속인 경우: 들어 오는 접속이 인터럽트된 CPU의 "squeue 인스턴스"에 할당됩니다. 아웃바운드인 경우에 접속은 어플리케이션이 실행되고 있는 CPU의 squeue 인스턴스에 할당됩니다.
  • NIC가 CPU보다 고속인 경우: 1개의 CPU로 NIC를 처리할 수 없습니다. 접속은 이용 가능한 모든 squeue에 무작위로 바인드됩니다.

Solaris 10 OS인 경우 NIC가 CPU보다 고속인지 저속인지는 시스템 관리자가 전역변수 ip_squeue_fanout를 조정하여 판단할 수 있습니다. 기본값은 no fanout 입니다. 즉 인터럽트된 CPU에 접속하고 있는 squeue에 들어 오는 접속을 할당합니다. CPU를 오프라인으로 할 목적으로 이 CPU에 바인드되어 있는 워커 스레드는 해당 바인드를 삭제하고 CPU가 온라인으로 돌아오면 바인드를 복원합니다. 이로써 DR 기능이 올바르게 동작합니다. 접속의 패킷이 여러 개의 NIC에 도착하고 거기에 따라 여러 개의 CPU가 인터럽트를 당하면 이들은 항상 원래 접속이 설정된 squeue 상에서 처리됩니다. Solaris 10 OS에서는 수직 경계는 TCP 기반의 접속에 대해서만 제공됩니다. 수직 경계에 대한 인터페이스는 이것이 TCP 접속인 것이 결정된 뒤, TCP 레이어 및 IP 레이어에서 실시됩니다. 임의의 용도의 일반적인 수직 경계를 도입하기 위해 Solaris 10 OS의 업데이트가 예정되어 있습니다. squeue의 API는 다음과 같이 되어 있습니다.

squeue_t  *squeue_create(squeue_t *, uint32_t, processorid_t, void (*)(), \
                                               void *, clock_t, pri_t);
void      squeue_bind(squeue_t *, processorid_t);
void      squeue_unbind(squeue_t *);
void      squeue_enter(squeue_t *, mblk_t *, void (*)(), void *);
void      squeue_fill(squeue_t *, mblk_t *, void (*)(), void *);

squeue_create는 새로운 squeue를 인스턴스화하여 squeue_bind() /squeue_unbind()를 사용하고, 특정 CPU와 바인드 또는 언바인드를 실시합니다. 한번 작성된 squeue는 삭제되지 않습니다. squeue_enter()를 사용하여 squeue로 액세스를 시도하고, 입력 스레드는 전술한 모델에 근거하여 squeue의 처리 및 드레인이 허가됩니다. squeue_fill()은 워커 스레드 또는 다른 스레드에 의해서 처리되는 squeue 상의 패킷을 큐잉하기 위해 사용합니다.

2.3 IP Classifier

IP 접속의 팬 아웃 메커니즘은 3개의 해시 테이블로 구성됩니다.

  • 5터플의 해시 테이블 {프로토콜, 원격 IP 주소, 로컬 IP 주소, 원격 포트, 로컬 포트}. 정규화된 TCP(ESTABLISHED) 접속을 유지합니다.
  • 프로토콜, 로컬 주소, 로컬 포트로 구성된 3터플의 조회. 수신기를 유지합니다.
  • 프로토콜 수신기용의 1터플의 조회.

조회의 일부로서 접속 구조(모든 접속 정보의 슈퍼 세트)가 반환됩니다. 이 접속 정보는 conn_t라 불리며 다음의 추상 객체로 나타납니다.

typedef struct conn_s {
    kmutex_t conn_lock;        /* Lock for conn_ref */
    uint32_t conn_ref;         /* Reference counter */
    uint32_t conn_flags;       /* Flags */
    
    struct ill_s *conn_ill;    /* The ill packets are coming on */
    struct ire_s *conn_ire;    /* ire cache for outbound packets */
    tcp_t     *conn_tcp;       /* Pointer to tcp struct */
    void     *conn_ulp         /* Pointer for upper layer*/
    edesc_pf conn_send;        /* Function to call on read side */
    edesc_pf conn_recv;        /* Function to call on write side */
    squeue_t *conn_sqp;        /* Squeue for processing */
    
    
    /* Address and Ports */
    struct {
        in6_addr_t connua_laddr;     /* Local address */
        in6_addr_t connua_faddr;     /* Remote address. */
    } connua_v6addr;
#define     conn_src V4_PART_OF_V6(connua_v6addr.connua_laddr)
#define     conn_rem V4_PART_OF_V6(connua_v6addr.connua_faddr)
#define     conn_srcv6 connua_v6addr.connua_laddr
#define     conn_remv6 connua_v6addr.connua_faddr
    union {
        /* Used for classifier match performance */
        uint32_t conn_ports2;
        struct {
            in_port_t tcpu_fport;     /* Remote port */
            in_port_t tcpu_lport;     /* Local port */
        } tcpu_ports;
    } u_port;
#define     conn_fport u_port.tcpu_ports.tcpu_fport
#define     conn_lport u_port.tcpu_ports.tcpu_lport
#define     conn_ports u_port.conn_ports2
    uint8_t     conn_protocol;     /* protocol type */
    kcondvar_t     conn_cv;
} conn_t;

여기서 주목해야 할 구성원은 squeue 또는 수직 경계로의 포인터입니다. 조회는 경계의 외부에서 행해지고 패킷은 접속처의 squeue로 처리 또는 큐잉됩니다. 또한 conn_recvconn_send 는 읽기 측과 쓰기 측의 함수를 포인트합니다. 패킷이 TCP용인 경우 읽기 측 함수는 tcp_input일 가능성도 있습니다.

또한 접속 팬 아웃 메커니즘에는 INADDR ANY 등 와일드 카드 수신기를 지원하는 기능이 준비되어 있습니다. 현재 접속 테이블과 바인드 테이블은 주로 TCP 및 UDP 전용입니다. 수신기 항목은 listen() 호출을 받는 동안에 작성됩니다. TCP의 3선 핸드셰이크가 완료한 후에 접속 테이블에 항목이 작성됩니다.

IP Classifier API는 다음과 같습니다.

conn_t     *ipcl_conn_create(uint32_t type, int sleep);
void       ipcl_conn_destroy(conn_t *connp);

int        ipcl_proto_insert(conn_t *connp, uint8_t protocol);
int        ipcl_proto_insert_v6(conn_t *connp, uint8_t protocol);
conn_t     *ipcl_proto_classify(uint8_t protocol);
int        *ipcl_bind_insert(conn_t *connp, uint8_t protocol, ipaddr_t src,
           uint16_t lport);
int        *ipcl_bind_insert_v6(conn_t *connp, uint8_t protocol,
           const in6_addr_t * src, uint16_t lport);
int        *ipcl_conn_insert(conn_t *connp, uint8_t protocol, ipaddr_t src,
           ipaddr_t dst, uint32_t ports);
int        *ipcl_conn_insert_v6(conn_t *connp, uint8_t protocol,
           in6_addr_t *src, in6_addr_t *dst, uint32_t ports);
void       ipcl_hash_remove(conn_t *connp);
conn_t     *ipcl_classify_v4(mblk_t *mp);
conn_t     *ipcl_classify_v6(mblk_t *mp);
conn_t     *ipcl_classify(mblk_t *mp);

함수의 이름은 해당 내용을 있는 그대로 나타내고 있습니다.

2.4 동기화 메커니즘

수직 경계에 의해서 실현되는 CPU 단위의 직렬화를 제외하면 스택은 완전하게 멀티 스레딩되어 있기 때문에 참조 기반의 방식을 사용하고 필요한 때에 접속 인스턴스를 사용할 수 있도록 하고 있습니다. 참조 카운트는 conn_t 구성원의 conn_ref로 마운트되어 conn_lock로 보호되고 있습니다. 잠금의 주된 목적은 conn_t의 대부분이 아니고 참조 카운트를 보호하는 것입니다. 일부 엔티티가 데이터 구조를 참조할(나중에 처리하는 데이터 구조에 포인터를 저장함) 때마다 CONN_INC_REF 매크로를 호출해 참조 카운트를 증분하고(기본적으로 conn_lock을 취득), conn_ref를 증분하고 conn_lock을 폐기합니다. 엔티티가 접속 인스턴스에의 참조를 폐기할 때마다 CONN_DEC_REF매크로를 사용해 그 참조를 폐기합니다.

설정된 TCP 접속에는 3개의 참조가 있습니다. 각 프로토콜 레이어는 인스턴스(TCP 와 IP에 대해 각 1개)에 대한 참조를 가집니다. 또한 분류자 자체도 설정된 접속이기 때문에 참조를 가집니다. 패킷이 접속처에 도착하여 분류자가 접속 인스턴스를 검색할 때마다 새로운 참조가 발생하지만, 프로토콜 레이어가 해당 패킷의 처리를 종료하면 폐기됩니다. 마찬가지로 접속 인스턴스 상에서 실행되는 타이머는 타이머가 기동했을 때에 언제라도 인스턴스가 동작할 수 있도록 참조를 갖추고 있습니다. 접속 인스턴스가 관련된 메모리는 마지막 참조가 폐기되면 해방됩니다.


3.0 TCP

Solaris 10 OS에 있어서의 TCP를 다루는 방법은 이전 릴리스와 같습니다. 즉 TCP는 클론 장치처럼 보이지만 실제로는 TCP 및 IP 코드가 1 개의 D_MP STREAMS 모듈로 통합된 복합물입니다. 통합된 TCP/IP 모듈의 열기와 닫기의 STREAMS 진입점은 IP의 진입점, 다시 말하자면 ip_open, ip_close와 같습니다. 오픈 시에 전달되는 주요 번호에 근거하여 IP는 오픈이 TCP 오픈에 대응할지 IP오픈에 대응할지를 판단합니다. TCP의 put 및 service STREAMS 진입점은 tcp_wput, tcp_wsrvtcp_rsrv입니다. tcp_wput 진입점은 단지 래퍼 루틴으로서의 역할을 수행하는 것이며 sockfs및 상위에서 전달되는 다른 모듈이 STREAMS를 사용해 TCP와 패킷 주고받기를 실현합니다. IP는 TCP 함수를 직접 호출하기 위해 tcp_rput가 없다는 것에 주의하십시오. IP의 STREAMS 진입점은 바뀌지 않습니다.

TCP의 조작 부분은 그림 1 에 나타나고 있듯이 squeue_*초기값을 통해서 입력되는 수직 경계에서 완전하게 보호되고 있습니다. 상위에서 내려오는 패킷은 래퍼 함수 tcp_wput을 통해서 TCP에 들어 옵니다. 이어서 대응하는 수직 경계에 들어간 다음 real TCP 출력 처리 함수 tcp_output의 실행을 시도합니다. 마찬가지로 하위에서 올라오는 패킷은 수직 경계에 들어간 후 real TPC 입력 처리 함수 tcp_input의 실행을 시도합니다. 수직 경계를 경유하여 TCP로 통하는 진입점은 다수 존재합니다.

図 1

그림 1

수직 경계가 사용하는 TCP 진입점:

tcp_input - All inbound data packets and control messages
tcp_output - All outbound data packets and control messages
tcp_close_output - On user close
tcp_timewait_output - timewait expiry
tcp_rsrv_input - Flow control relief on read side
tcp_timer - All tcp timers

3.1 TCP와 IP 사이의 인터페이스

FireEngine에서는 TCP와 IP 사이의 인터페이스가 제어 경로와 데이터 경로 양쪽 모두 STREAMS 기반의 메시지 입출력 인터페이스에서 함수 호출 기반의 인터페이스로 변경되었습니다. 아웃바운드 측에서는 수직 경계의 내부에 있으면서 TCP는 ip_output을 호출함으로써 완전하게 준비가 완료된 패킷을 IP에 직접 전달합니다.

마찬가지로 제어 메시지도 함수 인수로서 직접 전달됩니다. ip_bind_v{4, 6}은 바인드 메시지를 인수로서 전달 받아 필요한 처리를 실행하여 호출자에게 결과 mp 를 반환합니다. TCP는 connect() , bind() listen() 경로 안에서 ip_bind_v{4, 6}를 직접 호출합니다. IP는 여전히 모든 STREAMS 진입점을 보유하고 있지만 TCP(/dev/tcp)가 실제 장치 드라이버가 됩니다(즉, 다른 장치 드라이버가 대신하여 처리할 수 없습니다).

기본적인 프로토콜 처리 코드는 변경되지 않았습니다. 일반적인 소켓 호출 및 프레임워크와의 상호작용에 관해 살펴봅시다.

3.2 소켓

TCP의 소켓 개방 또는 /dev/tcp의 개방은 최종적으로 ip_open을 호출합니다. 개방은 다음에 IP 접속 분류자를 호출하여 이미 conn_t에 통합되어 있는 TCP당 종단점 제어 블록을 할당합니다. 이 접속의 squeue가 선택됩니다. 내부 개방인 경우 즉 억셉터 STREAMS의 sockfs를 사용하는 경우에는 거의 아무 작업도 이루어지지 않고, 수락 시까지 유효한 작업이 지연됩니다.

3.3 바인드

tcp_bind 는 최종적으로 IP와 대화하고 전달받은 주소가 유효한지 파악할 필요가 있습니다. FireEngine TCP는 이 요청을 통상의 TPI 메시지 형식으로 준비했습니다. 그러나 이 메시지는 함수 인수로서 ip_bind_v{4, 6}로 직접 전달되고 이 함수가 결과를 다른 메시지로 반환합니다. 메시지를 파라미터로서 사용함으로써 기존의 코드를 약간만 변경하여 활용할 수 있습니다. TCP가 바인드를 검증하기 위해서 사용하는 포트 해시 테이블은 분류자에서는 필요하지 않기 때문에 TCP에 그대로 남아 있습니다.

3.4 접속

tcp_connect 에서의 변경은 tcp_bind와 유사합니다. 완전한 bind() 요청은 TPI 메시지로서 준비되어, 함수 인수로서 ip_bind_v{4, 6}에게 전달됩니다. IP 는 분류자를 호출하여 접속된 해시 테이블에 접속을 삽입합니다. TCP 내의 conn_ hash 테이블은 사용되지 않습니다.

3.5 수신

이 경로는 tcp_bind의 일부입니다. tcp_bind는 로컬 바인드 TPI 메시지를 준비하고 이를 함수 인수로서 ip_bind_v{4, 6}에 전달합니다. IP는 분류자를 호출해서 해당 접속을 연결된 해시 테이블에 삽입합니다.

3.6 수락

Solaris 10 이하 릴리스에서 accept마운트에서는 수신기 컨텍스트 내에서 접속 설정을 처리하는 경우가 많았습니다. 3선 핸드셰이크는 수신기의 경계에서 완료되고 접속 지시가 수신기의 STREAMS에 송신됩니다. 수락을 실행하는데 필요한 메시지가 수신기의 STREAMS에 송신되고 수신기는 T_CONN_RES 메시지를 TCP에 송신하는 시점부터 sockfs가 긍정적인 응답을 수신할 때까지 싱글 스레드화되어 있었습니다. Solaris 10 이하 릴리스에서는 접속 도착율이 높은 경우 새로운 접속을 받아 들이는 스택의 능력은 크게 저하되었습니다.

게다가 TCP의 오버헤드가 추가되면 수락율이 더욱 떨어졌습니다. sockfs가 수신 측의 STREAMS를 TCP에게 열고 새로운 접속을 받아 들이는 경우 새로운 접속에 필요한 데이터 구조가 이미 할당되어 있다는 것을 TCP가 인식하지 못했기 때문에 새로운 구조를 할당하여 초기화했습니다. 나중에 수락 처리 중에 이러한 구조는 개선되었습니다. Solaris 10 이하 릴리스에서는 새롭게 생성된 접속의 패킷이 수신기의 경계에 도착한다는 큰 문제도 있었습니다. 모든 착신 패킷에 대해서 확인할 필요가 있었고 잘못된 경계에 도착한 패킷을 올바른 경계로 송신해야 했기 때문에 추가 지연이 발생했습니다.

FireEngine 모델에서는 SYN 패킷의 도착과 동시에 해당 경계 내에서 "접속 요청"을 설정함으로써(들어 오는 접속은 수락이 완료할 때까지 "접속 요청"이라고 불립니다) 해당 패킷은 항상 올바른 접속처를 찾아갑니다. 그 결과 TCP 전역 큐를 완전하게 제거하는 것이 가능합니다. 접속 지시는 계속해 수신기의 STREAM에 송신되지만 새롭게 작성된 수신 측의 STREAM에서 수락이 발생하여(이 때문에 이 STREAMS의 데이터 구조를 할당할 필요가 없습니다) 확인 응답을 수신 측의 STREAMS에서 송신할 수 있습니다. 그 때문에 sockfs는 수락 처리의 어느 시점에서도 싱글 스레드화될 필요가 없습니다.

새로운 들어 오는 접속(요청)이 존재하는 것은 해당 수신기가 있기 때문일 뿐이며 접속 요청이나 수신기도 접속 요청이 초기화를 수신하거나 수신기를 닫으면 수락 처리 중 임의의 시점에서 소멸할 가능성이 있기 때문에 새로운 모델에서는 신중을 기하여 구현했습니다.

수신기를 닫았을 경우에서도 수신기에 대한 접속 요청 참조가 항상 유효가 되도록 수신기에게 참조를 배치함으로써 접속 요청이 개시됩니다. 3선 핸드셰이크 완료 후에 접속 지시를 송신할 필요가 있는 경우에 접속 요청은 초기화를 수신하여 닫더라도 참조가 계속해 유효하도록 자신에게 참조를 배치합니다. 접속 요청은 접속 지시 메시지의 일부로서 자신에게 포인터를 송신하지만, 이것은 수신기가 닫지 않았다는 것을 확인하고 나서 수신기의 STREAMS를 이용해 송신됩니다. T_CONN_RES 메시지가 새롭게 작성된 억셉터의 STREAMS에 도착하면 다시 접속 요청의 경계에 들어가 수락 처리를 완료하기 전에 초기화 수신으로 인해 요청이 닫히지 않았다는 것을 확인합니다. TLI/XTI 기반의 어플리케이션인 경우 T_CONN_RES 메시지는 역시 수신기의 STREAMS에서 처리되어 확인 응답이 수신기의 STREAMS에 송신되므로 동작에 변화는 없습니다.

3.7 닫기

닫는 큐와 TCP에 대한 참조가 분리되었기 때문에 TCP 내의 닫기 처리는 참조 카운트가 제로가 될 때까지 기다릴 필요가 없어졌습니다. 닫는 큐에 대한 모든 참조가 없어지는 것과 동시에 닫기는 종료됩니다. TCP 데이터 구조 자체는 대부분의 경우 분리된 TCP로서 계속해서 존재할 수 있습니다. TCP에 대한 마지막 참조가 해제되면 TCP 데이터 구조가 해방됩니다.

사용자가 실행시키는 닫기는 스트림만을 닫습니다. 기본이 되는 TCP 구조는 계속 존재합니다. TCP는 모든 사용자 데이터를 전송한 후에 Peer와의 FIN/ACK 교환을 경유하고, 일정 시간 동안 TIME_WAIT 상태에 들어갑니다. 이것을 분리된 TCP라고 부릅니다. 분리된 TCP에서도 아웃바운드 및 인바운드의 처리가 분리된 특정 TCP에서 동시에 발생하지 않도록 보호할 필요가 있습니다.

3.8 데이터 경로

TCP는 IRE에 액세스할 수 있다면 대부분의 경우 아웃바운드 패킷을 송신하는데 IP를 호출할 필요도 없습니다. 통합된 TCP/IP 에서는 접속용으로 캐시된 IRE에 액세스할 수 있다는 장점이 있고, TCP는 IRE 내의 정보에 기반하여 데이터를 링크 레이어 드라이버에 직접 배치할 수 있습니다. FireEngine의 처리 방법은 전술한 바와 같습니다.

3.9 TCP 루프백

TCP Fusion는 Solaris 10 OS에서의 루프백 TCP 접속용 프로토콜리스 데이터 경로입니다. 2개의 로컬 TCP 종단점의 융합은 접속이 설정될 때 발생합니다. 기본값으로는 모든 루프백 TCP 접속이 융합됩니다. 이 동작은 튜닝 가능한 시스템 변수 do tcp fusion을 0으로 변경할 수 있습니다. 성공적인 융합을 수행하려면 양쪽 종단점 모두에서 다양한 조건을 충족해야 합니다.

  • 공통의 squeue를 공유할 필요가 있다.
  • TCP이어야 하며 "원시 소켓"이어서는 안 된다.
  • 프로토콜 수준의 처리를 요구하지 않는다. 즉 접속에 대해서 IPsec나 IPQoS 정책이 존재하지 않는다.

융합이 실패했을 경우는 통상의 TCP 데이터 경로를 사용하고, 성공했을 경우는 양쪽 모두의 종단점을 송신 경로로서 tcp fuse output()을 사용하게 됩니다. tcp fuse output()은 peer의 수신 큐에 어플리케이션 데이터를 직접 넣습니다. 프로토콜 처리는 이루어지지 않습니다. 데이터 큐잉 후 송신 측은 다음의 몇 개의 조작을 실행할 수 있습니다.

  • putnext(9F)를 호출하고 데이터를 수신 측의 읽기 큐에 배치한다.
  • 그대로 돌아와 동기 STREAMS 진입점을 이용해 수신 측에 큐잉 데이터를 취득시킨다.

동기 STREAMS가 유효한 경우는 후자의 경로를 취합니다. 모듈의 삽입 또는 삭제가 원인으로 TCP 모듈의 상위에서 sockfs가 직접 존재하지 않는 경우는 자동적으로 무효입니다.

TCP Fusion 안에 있는 잠금은 squeue와 상호 배타적인 tcp fuse lock에 의해 처리됩니다. 융합이 성공하는 조건 중의 하나는 양쪽 모두의 종단점이 같은 squeue를 사용하는 것입니다. 이렇게 하면 한쪽 편이 데이터를 계속 송신하고 있는 동안에 다른 한쪽 편이 소멸되지 않습니다. 동기 STREAMS가 유효한 경우 squeue만으로는 안전한 액세스를 실현하는데 충분하지 않습니다. tcp fuse rrw()는 squeue에 들어가지 않고 tcp rcv 목록과 그 외의 융합 관련 필드로의 액세스를 송신 측과 동기화할 필요가 있기 때문입니다. tcp fuse lock은 이러한 목적으로 사용됩니다.

동기 스트림 모드의 TCP Fusion에 대하여 작은 쓰기 흐름 제어의 속도 제한은 각각 다른 제한으로 설정되어 있는 수신 버퍼의 크기와 데이터 블록의 수를 확인하는 것에 의해서 실현됩니다. 이는 누적 크기의 확인이 데이터 블록 카운트의 확인보다 우선하는 표준 STREAMS flow control와는 다릅니다(STREAMS 큐의 고위 경계치는 보편적으로 바이트를 의미합니다). 각 큐잉에 의해서 수신 프로세스에 통지가 보내집니다. 데이터 블록의 증가는 수신 측이 저속인 것을 나타내고 있어 송신 측은 시스템 리소스를 더 이상 낭비하지 않기 위해 가능한 한 빠른 단계에서 블록 또는 통지될 필요가 있습니다. 실제 이것은 시스템 내의 미처리 세그먼트 수를 제한하는 것과 같습니다.

큐잉 가능한 데이터 블록의 최소수의 기본값은 8입니다. 이 값은 시스템에 걸쳐 조정 가능한 tcp_fusion_burst_min을 사용하여 보다 큰 값 또는 0으로 변경할 수 있습니다(후자의 값을 설정하면 버스트 확인이 비활성화 됨).


4.0 UDP

Solaris 10 OS에서는 프레임워크의 개선과는 별도로 스택 내에서의 UDP 패킷의 이동에 변경이 추가되었습니다. 프로젝트의 내부 코드명은 "Yosemite"였습니다. Solaris 10 이하 릴리스의 UDP 처리 비용은 패킷 단위의 처리 비용과 단위의 처리 비용이 균등하게 나뉘어 있었습니다. 통상 패킷 처리 비용은 STREAMS, 스트림 헤드 처리 및 스택과 드라이버 내에서의 패킷 폐기에 의해 발생했습니다. 바이트당 처리 비용은 H/W 체크섬의 결여와 네트워크 스택 전반에 걸쳐 최적화되지 않은 코드 분기에 의해 발생했습니다.

4.1 스택내에서의 UDP 패킷의 폐기

UDP는 신뢰성이 낮다고 알려져 있지만 로컬 영역 네트워크는 신뢰성이 매우 높아지고 있고 어플리케이션은 LAN 환경 내에서의 패킷 손실이 없다고 여겨지고 있는 추세입니다. 이 가정에 대한 논란은 거의 없지만 Solaris 10 이하 릴리스에서 스택은 UDP 과부하 처리에 관해서 그다지 효율적이지 못했고 스택 자체 안에서 패킷을 폐기하는 경향이 있었습니다.

인바운드에서는 전체적인 수신 경로에서 여러 개의 레이어에서 패킷이 폐기되고 있었습니다. UDP의 경우 패킷이 폐기되는 가장 일반적이고 분명한 장소는 패킷을 큐잉하는 리소스가 부족한 IP 레이어였습니다. 패킷의 폐기가 발생하는 또 하나의 중요하고 분명한 장소는 네트워크 어댑터 레이어입니다. 이런 종류의 폐기는 대체적으로 시스템에서 착신율이 높은 패킷을 처리하고 있을 때 발생합니다.

UDP sockfs 확장(sockudp)는 소켓 기반의 UDP 어플리케이션의 처리에 사용된 socktpi의 대체 경로입니다. 전자는 스트림 헤드와 TPI 메시지를 주고받는 인터페이스를 생략하여 어플리케이션과 네트워크 스택 사이에 보다 직접적인 채널을 제공합니다. 이로써 소켓과 트랜스포트 레이어를 통해서 직접적인 데이터 및 함수 액세스가 가능하게 됩니다. 또한 스택의 효율이 향상되어 UDP H/W 체크섬 오프로드와 조합함으로써(단편화된 UDP인 경우에도) UDP 패킷이 스택 내에서 거의 폐기되지 않습니다.

4.2 UDP 모듈

이것은 IP와 같은 보호 도메인 아래에서 실행하는 완전 멀티 스레딩 UDP 모듈입니다. 이 모듈에 의해 트랜스포트(UDP)가 상하의 레이어와 밀접하게 통합됩니다. 또한 socktpi는 UDP를 직접 호출할 수 있습니다. 마찬가지로 UDP도 데이터 링크 레이어를 직접 호출할 수 있습니다. 사후 GLDv3를 도입한 환경이라면 데이터 링크 레이어도 트랜스포트를 직접 호출할 수 있습니다. 게다가 메시지 기반의 인터페이스를 사용하지 않고 유틸리티 함수를 직접 호출할 수 있습니다.

종단점의 상태를 변경하는 함수를 실행하는 경우 UDP에서는 종단점 단위에서의 배타적인 처리가 필요합니다. udp rput other()는 IP 옵션을 가지는 패킷과 그 패킷의 처리를 취급하여 종단점의 옵션 관련 상태를 업데이트합니다. udp wput other()는 종단점의 상태를 업데이트할 필요가 있는 connect(3SOCKET)등 상위에서 내려오는 제어 조작을 취급합니다. STREAMS의 환경에서는 공유 내부 경계의 진입점을 사용하여 qwriter inner()를 사용해 종단점에 배타 액세스함으로써 이 동기화가 실현되었습니다.

Solaris 10 모델은 STREAMS에 의존하지 않는 내부 경계를 사용해 전술한 동기화를 실현합니다. 그 자세한 내용은 다음과 같습니다.

  • udp enter(): UDP 종단점 경계에 들어갑니다. 즉, udp become writer()가 UDP 종단점 상에서 배타적으로 됩니다. 이것은 즉시 또는 경계가 나중에 배타적으로 사용 가능하게 되었을 때 배타적으로 호출되는 함수를 지정합니다.
  • udp exit(): UDP 종단점 경계에서 나옵니다.

상위 또는 하위로부터 UDP에 들어가려면 udp enter()를 사용합니다. 통상의 경우와 같이 이러한 경계에서는 잠금은 유지되지 않습니다. 배타적 모드가 종료되면 udp exit()를 호출하고 경계를 나옵니다.

이것을 지원하기 위해서 새로운 UDP 모델은 UDP MT HOT 모드 및 UDP SQUEUE 모드라고 하는 2개의 조작 모드를 도입하였습니다. UDP MT HOT 모드에서는 여러 개의 스레드가 동시에 UDP 종단점에 들어갈 가능성이 있습니다. 이는 표준 데이터의 송수신에 사용되어 putshared STREAMS 진입점과 유사합니다. 제어 조작과 그 외의 특수한 경우에 udp become writer()를 호출하여 종단점 단위로 배타적이 되어 UDP SQUEUE 모드로 전환됩니다. 정의 상 squeue는 conn t에 대한 액세스를 직렬화합니다. UDP 접속의 squeue에 보류 중이라는 메시지가 없는 경우 종단점은 MT HOT 모드로 돌아옵니다. 그 사이 종단점의 MT 스레드가 모두 종료되어 있지 않는 경우는 메시지가 종단점에 큐잉되어 UDP는 UDP MT QUEUED 모드 또는 UDP QUEUED SQUEUE 모드 중 하나인 일시 모드가 됩니다.

안정 모드에 있는 동안 UDP는 종단점에서 동작하는 스레드의 수를 추적합니다. udp reader count 변수는 UDP MT HOT 모드로 읽기 측으로서 종단점에 들어가는 스레드의 수를 나타냅니다. UDP SQUEUE로의 이행은 읽기 측의 스레드가 1개로만 이루어 졌을 경우, 예를 들어 이 카운터가 1이 되었을 경우에 발생합니다. 마찬가지로 udp squeue count는 UDP SQUEUE 모드에서 종단점의 squeue로 동작하는 스레드의 수를 나타냅니다. UDP MT HOT에의 모드 이행은 마지막 스레드가 종단점을 나온 뒤에 발생합니다.

UDP와 IP는 같은 보호 도메인에서 실행되지만 이것들은 전혀 다른 STREAMS 모듈입니다. 이 때문에 STREAMS의 플러밍은 그대로 보관 유지되어 UDP 모듈 인스턴스는 항상 IP 상에 배치됩니다. 이는 모든 UDP 종단점에 대해 여분의 열기와 닫기를 발생시키지만, 스트림에 I POP를 발행해 IP9 에 직접 액세스 하는 등 특정 처리를 이러한 플러밍 기하학에 의존하여 실시하는 어플리케이션에 대해서 하위 호환성을 제공합니다.

실제의 UDP 처리는 IP 인스턴스 내에서 이루어집니다. UDP 모듈 인스턴스는 종단점에 관한 상태를 가지고 있는 것이 아니라 더미 모듈로서 동작하는 것에 지나지 않습니다. 그 목적은 STREAMS 플러밍의 모양을 바꾸지 않는다는데 있습니다.

Solaris 10 플랫폼에서는 다음의 플러밍 모드를 사용할 수 있습니다.

  • 표준: 최초로 IP가 오픈되고 그 후에 UDP가 직접 상위에 배치됩니다. 이것은 UDP 소켓 또는 장치가 오픈될 때의 기본 동작입니다.
  • SNMP: UDP가 IP 이외의 모듈의 상위에 배치됩니다. 이 경우 SNMP 의미론만이 지원됩니다.

이러한 모드는 IP와 UDP 사이의 중간 모듈이 지원되어 있지 않은 것을 의미합니다. IP와 트랜스포트 모듈 사이의 레이어간 통신의 의미론은 비공개이므로 실제 Solaris 기술에서는 지금까지 이러한 시나리오를 지원하지 않았습니다.

4.3 UDP와 소켓의 대화

socket(3SOCKET) 시스템 호출 중에 발생하는 중요한 이벤트에 소켓의 주소 패밀리 및 프로토콜 타입에 관련된 모듈의 플러밍이 있습니다. TCP 또는 UDP 소켓은 대부분의 경우 대응하는 트랜스포트 모듈의 직접 상위에 위치하는 sockfs를 발생시킵니다. Solaris 10 이하 릴리스의 소켓 레이어는 STREAMS 초기값을 사용해 UDP 모듈과 통신하고 있었습니다. Solaris 10 OS에서는 함수 호출이 가능한 인터페이스가 사용되고 있고, sockfs로부터 UDP에 대한 각 송신 시에 메타 데이터인 T UNITDATA REQ 메시지를 사용할 필요가 없습니다. 대신에 데이터와 그 부속 정보(원격 소켓 주소 등)를 대체 UDP 진입에 직접 제공하여 여분으로 할당하는 비용을 절약할 수 있습니다.

트랜스포트 모듈의 경우 sockfs의 바로 아래에 위치하고 있으면 동기 STREAMS를 사용할 수 있습니다. 이 때문에 트랜스포트 레이어는 착신 데이터를 버퍼링하고, 읽기 조작이 이루어졌을 때 어플리케이션이 나중에 데이터를 취득할 수 있도록(동기 STREAMS에 의한다) 수신 처리 시간이 단축됩니다.

4.4 동기 STREAMS

동기 STREAMS는 메시지를 주고 받거나 처리하는 것을 목적으로 하는 기존의 STREAMS 인터페이스의 확장 기능입니다. 처음에는 복사와 체크섬을 조합한 기능의 일부로서 추가되었습니다. 이 기능에 의해 모듈 또는 드라이버의 진입점을 사용자의 I/O 요청에 대해 동기 형식에서 호출할 수 있습니다. 기존의 STREAMS에서는 이러한 요청에 대해서 스트림 헤드가 동기 배리어입니다. 동기 STREAMS는 이 배리어를 스트림 헤드로부터 아래의 모듈로 이동하는 메커니즘을 제공합니다.

동기 STREAMS의 TCP 마운트는 다양한 이유로 Solaris 10 이하 릴리스에서는 복잡했습니다. 가장 큰 이유 중 하나는 체크섬과 copyin/copyout 처리의 결합이었습니다.

이에 비하여 Solaris 10 OS의 경우는 TCP는 copyin/copyout 할 때 체크섬에 의존하지 않기 때문에 읽기 측에서의 루프백 TCP 및 UDP로 사용할 수 있도록 메커니즘이 대폭적으로 간소화되었습니다. 동기 STREAMS 진입점은 read(2)recv(3SOCKET) 등의 요청 안에서 호출됩니다. 이러한 모듈에서는 putnext(9F)를 사용해 데이터 업스트림을 송신하는 대신에 자신의 내부 수신 큐에 데이터를 넣기 때문에 송신 스레드는 곧바로 돌아올 수 있습니다. strrput()을 호출하고 송신 스레드 컨텍스트 내부에서 스트림 헤드로 데이터를 큐잉하지 않아도 되기 때문에 보다 동적인 동작 실현이 가능합니다. 큐잉 및 수신 측 어플리케이션에 대한 시그널/폴링 통지에 걸리는 시간을 단축하는 것으로 송신 스레드는 곧바로 돌아와 다른 작업을 실행할 수 있습니다. 그 때문에 직렬화되는 조작도 지금까지보다 훨씬 간소화됩니다.

데이터가 도착할 때마다 트랜스포트 모듈은 어플리케이션에 의한 데이터 취득을 스케줄합니다. 읽기 처리 시에 어플리케이션이 블록 상태인 경우(sleeve 상태)는 블록이 해제되고 실행이 재개됩니다. 이것은 스트림 상에서 STR WAKEUP SET()의 호출을 계기로 실행됩니다. 마찬가지로 어플리케이션용의 데이터가 없는 경우는 STR WAKEUP CLEAR()를 호출함으로써 다음의 읽기 실행 중에 어플리케이션을 다시 블록할 수 있습니다. 그것보다 먼저 새로운 데이터가 도착하는 경우는 이 상태가 무효가 되어 이후의 읽기 처리가 계속됩니다.

어플리케이션은 읽기 이벤트가 발생할 때까지 poll(2)에서 블록되는 경우가 있습니다. 또 사용되고 있는 소켓이 블록되지 않은 경우는 SIGPOLL 또는 SIGIO 시그널을 기다리기도 합니다. 이 때문에 트랜스포트 모듈은 데이터를 수신할 때마다 이벤트 통지를 전달하거나 어플리케이션에 통지하기도 합니다. 이는 대응하는 스트림상에서 STR SENDSIG()을 호출함으로써 실행됩니다.

읽기 처리의 일부로서 트랜스포트 모듈은 읽기 측의 동기 STREAMS 진입점으로부터 데이터를 돌려주는 형식으로 어플리케이션에 데이터를 전달합니다. 루프백 TCP의 경우 동기 STREAMS의 읽기 진입점은 해당 수신 큐의 내용 전체(바이트 스트림)를 스트림 헤드에 반환합니다. 나머지 데이터는 다음의 읽기를 기다리는 스트림 헤드로 큐잉됩니다. UDP의 경우 읽기 진입점은 한 번에 1개의 메시지(데이터 그램)만을 반환합니다.

4.5 STREAMS 폴백

디폴트에서는 sockfs가 대응하는 트랜스포트 모드의 바로 위에 있는 경우 모든 UDP 및 루프백 TCP 소켓에 대해서 직접 전송과 읽기 측의 동기 STREAMS 최적화가 유효로 되어 있습니다. 이들 기능을 무효로 하는 것이 필요한 몇 가지 경우가 있습니다. 이 경우 sockfs와 트랜스포트 모듈 사이의 메시지 교환을 putnext(9F)를 통해서 실시할 필요가 있습니다. 다음과 같은 경우가 이에 해당합니다.

  • 중간 모듈: 모듈은 autopush(1M)에 의해 오픈 시에 트랜스포트 모듈의 상위에 자동 배치되도록 구성되어 있거나 ioctl(2)을 통해서 소켓에 I PUSH 됩니다.
  • 스트림 변환: 가공의 sockmod 모듈이 소켓으로부터 I POP되어 소켓 종단점부터 장치 스트림으로 변환됩니다.

(소켓 종단점에서는 I INSERT 또는 I REMOVE ioctl 은 허가되지 않기 때문에 그 처리에 폴백은 필요없다는 점에 주의 바랍니다.)

폴백이 필요한 경우 sockfs는 직접 모드가 무효라는 점을 트랜스포트 모듈에게 통지합니다. 통지는 sockfs 모듈에 의해서 ioctl 메시지 형식으로 송신됩니다. 이것은 트랜스포트 모듈에 대하여 데이터 업스트림을 송신하려면 putnext(9F)를 사용할 필요가 있는 것을 전달합니다. 이로써 데이터는 중간 모듈을 이동해 장치 스트림 시멘틱스와의 호환성이 제공됩니다.


5.0 IP

전술한 바와 같이 모든 트랜스포트 레이어는 STREAMS 모듈과 마찬가지로 완전하게 멀티 스레딩되어 유사 장치 드라이버로서 동작하는 IP 모듈에 통합되어 있습니다. IP에 있어서의 주요한 변경은 IP 클라이언트 기능의 삭제와 인바운드 패킷 스트림의 다중화였습니다. 새로운 IP Classifier(여전히 IP 모듈의 일부)의 역할은 인바운드 패킷을 적절한 접속 인스턴스로 분류하는 것입니다. IP 모듈은 계속해서 네트워크 레이어 프로토콜의 처리와 네트워크 인터페이스의 플러밍 및 관리를 취급합니다.

새로운 스택 내에서의 네트워크 인터페이스, 다중 경로 및 멀티 캐스트의 플러밍의 동작 시스템에 대해 살펴 봅시다.

5.1 NIC의 플러밍

플러밍은 IP, ARP 및 장치 드라이버간에서 메시지 교환에 관련된 일련의 긴 처리를 말합니다. IOCTL SET의 대부분은 통상 플러밍 처리와 관계가 있습니다. 보통 모델에서는 이러한 IOCTL를 ILL(IP Lower Level)마다 1씩 직렬화합니다. 예를 들어 hme0qfe0의 플러밍은 서로 간섭하는 일 없이 병렬해서 진행됩니다. 다만 hme0상의 다양한 IOCTL SET은 모두 직렬화됩니다.

한층 더 꼼꼼한 방법을 사용하여 IIL마다가 아니라 IPIF마다 처리를 직렬화한다는 방법도 생각할 수 있습니다. 이 방법은 많은 IPIF가 ILL 상에서 호스트되어 있고, 다른 IPIF 상의 처리가 서로 간섭하지 않는 경우만 효과가 있습니다.

게다가 표준의 Solaris MT 수법을 사용하여 모든 IOCTL를 완전하게 멀티 스레딩하는 방법도 생각할 수 있습니다. 그러나 이 방법은 필요이상으로 복잡하고 그만큼의 부가가치는 없습니다. 수신 및 드라이버나 다른 모듈과의 메시지 교환이 포함되는 플러밍 시퀀스 전체적으로 잠금을 유지하는 것은 곤란합니다. 순수한 반복적이지 않은 제어 처리를 이루어지기 때문에 IPIF 상에 여러 개의 IOCTL SET을 동시에 허가하는 것은 성능적으로도 기능적으로도 장점이 없습니다. 브로드캐스트 IRE는 IPIF 단위가 아닌 IIL 단위로 작성됩니다. 이 때문에 IIL 상에서 여러 개의 IPIF를 동시에 기동하려고 하면 IRE 작성 논리가 매우 복잡하게 됩니다. 한편 IIL 단위로의 플러밍 처리의 직렬화는 기존의 IP 코드 기반에 간단하게 도움이 됩니다. 플러밍 중에서 IP는 장치 드라이버 및 ARP와 메시지를 교환합니다. 기본이 되는 장치 드라이버로부터 수신한 메시지도 IP에서 배타적으로 처리됩니다. 쓰기 입 측과 읽기 측의 처리 과정 사이에 상호 배타를 제공하는 가운데 putnext에서는 표준의 상호 배타 잠금을 유지할 수 없기 때문에 이 방법은 편리합니다. 전배타적인 PERMOD syncq가 아니라 ILL 단위의 직렬화 큐를 사용함으로써 이 효과는 간단하게 실현될 수 있습니다.

5.2 IP 네트워크 다중 경로(IPMP)

IPMP 처리는 모두 IPMP 그룹 개념에 근거하여 이루어집니다. fail over 처리와 fail back 처리는 통상은 같은 IPMP 그룹에 포함되는 2개의 IIL 사이에서 이루어집니다. IPIF와 ILM는 IIL사이를 이동합니다. 이것에는 발신지 ILL가 하위로 이동하는 것이 포함되고, 경우에 따라서는 송신지 ILL가 상위로 이동하는 것이 포함됩니다. IIL의 상하에의 이동은 브로드캐스트 IRE에 영향을 줍니다. 브로드캐스트 IRE 는 브로드캐스트 패킷이 중복하여 수신되지 않도록 IPMP 그룹마다 그룹화할 필요가 있습니다. 이 때문에 브로드캐스트 IRE의 조작은 IPMP 그룹의 모든 멤버에게 영향을 줍니다. IFF_FAILED 또한 IFF_STANDBY를 설정하면 IPMP 그룹 내의 모든 ILL가 평가되어 브로드캐스트 IRE가 재그룹화됩니다. 이 때문에 IPMP 그룹 단위의 IPMP 처리의 직렬화는 기존의 코드 기반에 간단하게 도움이 됩니다. IPMP 그룹에는 IPv4 IIL과 IPv6 ILL의 양쪽 모두가 포함되어 있습니다.

5.3 멀티 캐스트

멀티 캐스트 조인은 ILG 구조와 ILM 구조의 양쪽 모두에서 동작합니다. 멀티 캐스트 조인을 실행하고자 하는 IPC(소켓) 상에서 동작하고 있는 여러 개의 스레드는 ILG로 동작할 경우에는 동기화할 필요가 있습니다. 멀티 캐스트 조인을 실행하고자 하는 다른 IPC(소켓 종단점) 상에서 동작하고 있을 가능성이 있는 여러 개의 스레드는 최종적으로 ILM을 동시에 조작하는 경우가 있으므로 ILM에 대한 액세스를 동기화할 필요가 있습니다. 두 경우 모두 표준적인 Solaris MT 수법에 따릅니다. 지금까지 언급한 플러밍, IPMP 및 멀티 캐스트를 모두 고려했을 경우 그 공통점은 IPMP 그룹 단위로 모든 배타적 연산을 직렬화하는 것입니다. IPMP가 유효하지 않은 경우는 phyint 하지만 존재합니다. 예를 들어 hme0 v4 ILL은 hme0 v6 ILL은 함께 phyint를 공유합니다. 전술한 예에서는 멀티 캐스트에는 잠재적으로 고도의 멀티 스레딩이 포함되어 있습니다. 다만, 다른 배타적 조작과 공존할 필요가 있습니다. 예를 들어 fail over 처리가 진행 중이며 2개의 ILL사이에 ILM을 이동시키려고 할 때 스레드가 ILM을 작성 또는 삭제하는 것은 바람직하지는 않습니다. 따라서 최소한의 공통점은 물리 인터페이스 또는 IPMP 그룹마다 멀티 캐스트 조인을 직렬화하는 것입니다.


6.0 Solaris 10 장치 드라이버 프레임워크

Solaris 10 OS 이전의 네트워크 장치 드라이버의 마운트 방법과 이것을 새로운 Solaris 10 스택으로 왜 변경해야 하는지에 대해서 간단하게 설명합니다.

6.1 GLDv2 및 모노리식 드라이버(Solaris 9 이하 릴리스)

Solaris 10 이하 릴리스에서는 네트워크 스택은 통상 2개의 방법 중 어느 하나가 마운트되는 DLPI1 공급자를 경유합니다. 다음 그림(그림 2)은 이른바 모노리식 Data Link Provider Interface(DLPI) 드라이버에 근거하는 스택과 Generic LAN Driver(GLDv2) 모듈을 이용하는 드라이버에 근거하는 스택을 나타내고 있습니다.

그림 2

그림 2

GLDv2 모듈은 기본적으로 라이브러리로서 동작합니다. 클라이언트는 계속 장치에 바인드되어 있는 드라이버 인스턴스와 교환하지만, DLPI 프로토콜 처리는 GLDv2 모듈을 호출함으로써 실행되는데 이것이 다음에 드라이버를 호출하여 하드웨어에 액세스합니다. GLD 모듈을 사용함으로써 드라이버의 개발자에게 거의가 범용적인 대량의 DLPI 프로토콜 처리를 재마운트하지 않아도 되는 분명한 이점이 있습니다. 802.1q 가상 LAN(VLAN) 등의 층 2(데이터 링크)개의 기능도 주로 GLD 모듈에 마운트할 수 있도록 모든 드라이버에서 이를 이용할 수 있습니다. 다만 802.3ad 링크 집적(trunking) 등 네트워크 인터페이스와 장치가 1:1로 대응하지 않는 기능을 마운트하는 경우 아키텍처에 문제가 생깁니다.

GLDv2 및 모노리식 드라이버는 양쪽 모두 DLPI 메시지에 의존하고 STREAMS 프레임워크를 통해서 상위 레이어와 통신합니다. 이 메커니즘은 링크 집적이나 10Gb NIC에서는 별로 효율적이 못했습니다. 새로운 스택에서는 데이터의 국소성을 실현하여 스택이 치밀하게 장치 드라이버를 제어해 인터럽트를 처리할 수 있는 뛰어난 메커니즘이 필요했습니다.

6.2 GLDv3 -- 새로운 아키텍처

Solaris 10 소프트웨어에는 새로운 스택과 함께 GLDv3(내부명 "project Nemo")라고 하는 새로운 장치 드라이버 프레임워크가 도입되었습니다. 주요한 장치 드라이버의 대부분은 이 프레임워크에 이식되어 향후의 모든 장치 드라이버나 10Gb 장치 드라이버는 이 프레임워크를 따르게 됩니다. 이 프레임워크에는(외부의 비 IP 모듈이 계속해 동작할 수 있도록) 하위 호환성용으로 STREAMS 기반의 DLPI 레이어도 준비되어 있습니다.

GLDv3 아키텍처는 네트워크 스택의 레이어 2를 가상화합니다. 네트워크 인터페이스와 장치 사이에 1:1 관계는 이미 존재하지 않습니다. 다음 그림(그림 3)은 MAC 서비스 모듈(MAC)에 등록된 여러 개의 장치를 나타내고 있습니다. 또한 DLPI를 통해서 데이터 링크 드라이버(DLD)와 통신하는 기존의 클라이언트와 커널 기반에서 데이터 링크 서비스 모듈(DSL)에 대하여 함수 호출을 직접 실시하는 클라이언트도 나타나고 있습니다.

그림 3

6.2.1 GLDv3 드라이버

GLDv3 드라이버는 GLD 드라이버와 많이 닮았습니다. 드라이버는 misc/mac. 및 misc/dld. 의 의존관계에 링크하고 있을 필요가 있습니다. 아래 구조의 인스턴스에 대한 포인터를 지정하여 mac_register()를 호출하고 MAC 모듈에 등록할 필요가 있습니다.

typedef struct mac {
    const char       *m_ident;
    mac_ext_t        *m_extp;
    struct mac_impl  *m_impl;
    void             *m_driver;
    dev_info_t       *m_dip;
    uint_t           m_port;
    mac_info_t       m_info;
    mac_stat_t       m_stat;
    mac_start_t      m_start;
    mac_stop_t       m_stop;
    mac_promisc_t    m_promisc;
    mac_multicst_t   m_multicst;
    mac_unicst_t     m_unicst;
    mac_resources_t  m_resources;
    mac_ioctl_t      m_ioctl;
    mac_tx_t         m_tx;
} mac_t;

이 구조는 등록의 유효기간 중 존속할 필요가 있습니다. 예를 들어 mac_unregister()가 호출을 받을 때까지 해방할 수 없습니다. GLDv3 드라이버 _init(9E) 진입점은 mod_install(9F)을 호출하기 전에 mac_init_ops()를 호출할 필요도 있고, 이들은 _fini(9E)로부터 mod_remove(9F)를 호출한 후에 mac_fini_ops()를 호출할 필요가 있습니다.

mac_t의 중요한 구성원을 아래에 나타냅니다.

m_impl - MAC 모듈에 의해서 사용되어 비공개 데이터를 포인트합니다. 드라이버가 읽기 및 변경을 할 수는 없습니다.

m_driver -이 필드는 드라이버에 의해서 설정되어 그 비공개 데이터를 포인트합니다. 이 값은 드라이버 진입점에 선두의 인수로서 지정됩니다.

m_dip - 이 필드는 mac_register()를 호출하는 드라이버 인스턴스의 dev_info_t 포인터로 설정할 필요가 있습니다.

m_stat -

typedef uint64_t    (*mac_stat_t)(void *, mac_stat_t);

이 진입점은 mac_stat_t 목록(아래와 같이)에서 정의되는 몇 개의 통계 정보의 값을 취득하기 위해서 호출됩니다. 모든 값은 64비트의 부호 없는 정수로 저장 및 반환됩니다. 드라이버가 명시적으로 지원 선언을 하고 있지 않은 통계 정보의 값은 요구되지 않습니다.

m_start -

typedef    int (*mac_start_t)(void *);

인터페이스가 등록되었을 때에 리셋/휴지 상태인 장치를 그 상태로부터 해제하기 위해서 이 진입점이 호출됩니다. 이 호출을 이루어질 때까지는 MAC 모듈이 전송용 패킷을 송신하지 않고 드라이버는 수신용의 패킷을 송신할 수 없습니다. 이 함수가 정상 종료하면 제로가 반환됩니다. 이상종료(ABEND)되면 해당하는 errno 값이 반환됩니다.

m_stop -

typedef void (*mac_stop_t)(void *);

이 진입점은 인터페이스의 등록을 해제할 수 있도록 장치를 정지해 리셋/휴지 상태로 합니다. 이 호출이 실행되면 MAC 모듈이 전송용 패킷을 송신하는 경우는 없고, 이 호출이 완료되면 드라이버는 수신용의 패킷을 송신할 수 없습니다.

m_promisc -

typedef int (*mac_promisc_t)(void *, boolean_t);

이 진입점은 장치의 혼재를 설정하기 위해 사용됩니다. 2번째의 인수가 B_TRUE인 경우 장치는 미디어 상의 모든 패킷을 수신합니다. 인수가 B_FALSE로 설정되어 있는 경우는 장치의 uni-cast 주소 및 미디어의 브로드캐스트 주소로 오는 패킷만을 수신합니다.

m_multicst -

typedef int (*mac_multicst_t)(void *, boolean_t, const uint8_t *);

이 진입점은 장치가 패킷을 수신하는 멀티 캐스트 주소 세트에 주소를 추가 또는 삭제하기 위해서 사용됩니다. 2번째의 인수가 B_TRUE인 경우 3번째의 인수로 포인트되는 주소가 세트에 추가됩니다. 2번째의 인수가 B_FALSE인 경우 3번째의 인수로 포인트되는 주소가 삭제됩니다.

m_unicst -

typedef int (*mac_unicst_t)(void *, const uint8_t *);

이 진입점은 새로운 장치 uni-cast 주소를 설정하기 위해 사용됩니다. 이 호출을 실행되면 장치가 휴지 모드인 경우를 제외하고 새로운 주소 및 미디어 브로드캐스트 주소를 가지는 패킷만을 수신합니다.

m_resources -

typedef void (*mac_resources_t)(void *, boolean_t);

이 진입점은 드라이버가 개개의 수신 리소스 또는 Rx 링을 등록하는 것을 요구하기 위해서 호출됩니다.

m_tx -

typedef mblk_t *(*mac_tx_t)(void *, mblk_t *);

이 진입점은 장치가 전송용의 패킷을 송신하기 위해서 사용됩니다. 2번째의 인수는 mblk_t 구조에 포함되는 1 이상의 패킷을 포인트합니다. 같은 패킷의 단편(fragment)은 b_cont 필드를 사용해 링크됩니다. 개개의 패킷은 선두의 단편(fragment) 내의 b_next 필드에 의해 링크됩니다. 패킷은 체인 내에서 출현하는 차례로 전송이 스케줄 됩니다. 스케줄할 수 없는 패킷의 나머지 체인은 반환됩니다. 스케줄할 수 없는 패킷을 m_tx()가 돌려주는 경우, 리소스가 사용 가능이 된 시점에 드라이버는 mac_tx_update()를 호출할 필요가 있습니다. 모든 패킷이 전송용으로 스케줄되어 있는 경우는 NULL이 반환됩니다.

m_info - 이것은 다음과 같이 정의되고 있는 편입 구조입니다.

typedef struct mac_info {
    uint_t        mi_media;
    uint_t        mi_sdu_min;
    uint_t        mi_sdu_max;
    uint32_t      mi_cksum;
    uint32_t      mi_poll;
    boolean_t     mi_stat[MAC_NSTAT];
    uint_t        mi_addr_length;
    uint8_t       mi_unicst_addr[MAXADDRLEN];
    uint8_t       mi_brdcst_addr[MAXADDRLEN];
} mac_info_t;

mi_media는 미디어 타입의 세트입니다. mi_sdu_min은 최소 유효 하중 크기입니다. mi_sdu_max는 최대 유효 하중 크기입니다.mi_cksum은 장치 cksum 기능 플래그의 자세한 내용입니다. mi_poll은 드라이버가 폴링을 지원하고 있는 경우의 자세한 내용입니다. mi_addr_length는 미디어가 사용하는 주소의 길이로 설정됩니다. mi_unicst_addrmac_register()가 호출된 시점에서의 장치의 uni-cast 주소를 사용하여 설정됩니다. mi_brdcst_addr는 미디어의 브로드캐스트 주소로 설정됩니다. mi_stat는 불리언 값의 배열입니다.

typedef enum {
        MAC_STAT_IFSPEED = 0,
        MAC_STAT_MULTIRCV,
        MAC_STAT_BRDCSTRCV,
        MAC_STAT_MULTIXMT,
        MAC_STAT_BRDCSTXMT,
        MAC_STAT_NORCVBUF,
        MAC_STAT_IERRORS,
        MAC_STAT_UNKNOWNS,
        MAC_STAT_NOXMTBUF,
        MAC_STAT_OERRORS,
        MAC_STAT_COLLISIONS,
        MAC_STAT_RBYTES,
        MAC_STAT_IPACKETS,
        MAC_STAT_OBYTES,
        MAC_STAT_OPACKETS,
        
        MAC_STAT_ALIGN_ERRORS,
        MAC_STAT_FCS_ERRORS,
        MAC_STAT_FIRST_COLLISIONS,
        MAC_STAT_MULTI_COLLISIONS,
        MAC_STAT_SQE_ERRORS,
        MAC_STAT_DEFER_XMTS,
        MAC_STAT_TX_LATE_COLLISIONS,
        MAC_STAT_EX_COLLISIONS,
        MAC_STAT_MACXMT_ERRORS,
        MAC_STAT_CARRIER_ERRORS,
        MAC_STAT_TOOLONG_ERRORS,
        MAC_STAT_MACRCV_ERRORS,
        
        MAC_STAT_XCVR_ADDR,
        MAC_STAT_XCVR_ID,
        MAC_STAT_XVCR_INUSE,
        MAC_STAT_CAP_1000FDX,
        MAC_STAT_CAP_1000HDX,
        MAC_STAT_CAP_100FDX,
        MAC_STAT_CAP_100HDX,
        MAC_STAT_CAP_10FDX,
        MAC_STAT_CAP_10HDX,
        MAC_STAT_CAP_ASMPAUSE,
        MAC_STAT_CAP_PAUSE,
        MAC_STAT_CAP_AUTONEG,
        MAC_STAT_ADV_CAP_1000FDX,
        MAC_STAT_ADV_CAP_1000HDX,
        MAC_STAT_ADV_CAP_100FDX,
        MAC_STAT_ADV_CAP_100HDX,
        MAC_STAT_ADV_CAP_10FDX,
        MAC_STAT_ADV_CAP_10HDX,
        MAC_STAT_ADV_CAP_ASMPAUSE,
        MAC_STAT_ADV_CAP_PAUSE,
        MAC_STAT_ADV_CAP_AUTONEG,
        MAC_STAT_LP_CAP_1000FDX,
        MAC_STAT_LP_CAP_1000HDX,
        MAC_STAT_LP_CAP_100FDX,
        MAC_STAT_LP_CAP_100HDX,
        MAC_STAT_LP_CAP_10FDX,
        MAC_STAT_LP_CAP_10HDX,
        MAC_STAT_LP_CAP_ASMPAUSE,
        MAC_STAT_LP_CAP_PAUSE,
        MAC_STAT_LP_CAP_AUTONEG,
        MAC_STAT_LINK_ASMPAUSE,
        MAC_STAT_LINK_PAUSE,
        MAC_STAT_LINK_AUTONEG,
        MAC_STAT_LINK_DUPLEX,
        MAC_STAT_LINK_STATE,
        MAC_NSTAT    /* must be the last entry */
} mac_stat_t;

매크로 MAC_MIB_SET(), MAC_ETHER_SET()MAC_MII_SET()은 3개의 각 그룹의 모든 값을 B_TRUE로 설정하기 위해 준비되어 있습니다.

6.2.2 MAC 서비스(MAC) 모듈

몇 개의 중요한 드라이버 지원 함수는 다음을 포함합니다.

mac_resource_add -

extern mac_resource_handle_t mac_resource_add(mac_t *,    mac_resource_t *);

다양한 구성원이 다음과 같이 정의됩니다.

typedef void (*mac_blank_t)(void *, time_t, uint_t);
typedef mblk_t *(*mac_poll_t)(void *, uint_t);


typedef enum {
        MAC_RX_FIFO = 1
} mac_resource_type_t;


typedef struct mac_rx_fifo_s {
    mac_resource_type_t    mrf_type;    /* MAC_RX_FIFO */
    mac_blank_t            mrf_blank;
    mac_poll_t             mrf_poll;
    void                   *mrf_arg;
    time_t                 mrf_normal_blank_time;
    uint_t                 mrf_normal_pkt_cnt;
} mac_rx_fifo_t;



typedef union mac_resource_u {
    mac_resource_type_t    mr_type;
    mac_rx_fifo_t          mr_fifo;
} mac_resource_t;

이 함수는 각각의 수신 리소스(일반적으로는 DMA 기술자의 링 버퍼)를 MAC 모듈에 등록하기 위해서 m_resources() 진입점으로부터 호출을 받습니다. 반환된 mac_resource_handle_t 값은 mac_rx()에 대한 호출로 지정됩니다. mac_resource_add()의 2번째의 인수는 추가되는 리소스를 지정합니다. 리소스는 mac_resource_t 구조로 지정됩니다. 현재는 MAC_RX_FIFO 타입의 리소스만이 지원됩니다. MAC_RX_FIFO 리소스는 mac_rx_fifo_t 구조로 기술됩니다.

mac_blank 함수는 장치의 인터럽트율을 제어하기 위해서 상위 레이어에 의해 사용됩니다. 1번째의 인수는 poll_blank의 1번째의 인수로서 사용되는 장치 컨텍스트입니다.

그 외의 필드의 mrf_normal_blank_timemrf_normal_pkt_cnt는 기본값의 인터럽트 간격과 패킷 카운트의 역치를 각각 지정합니다. 이러한 파라미터는 상위 레이어가 기본값의 인터럽트율로 반환하는 경우에 mac_blank의 2번째 및 3번째의 인수로서 사용될 가능성이 있습니다.

인터럽트율은 다른 인수를 지정하여 poll_blank를 호출함으로써 상위 레이어에 의해 제어됩니다. 인터럽트율은 mac_blank의 마지막 2개의 인수에 이러한 값의 배수를 건네줌으로써 상위 레이어에 의해 증감될 수 있습니다. 이러한 값을 제로로 설정하면 인터럽트가 무효가 되어 NIC는 폴링 모드라고 간주됩니다.

mac_poll은 상위 레이어에 의해서 사용되는 드라이버 독자적인 함수이며 Rx 링에서 패킷의 체인을 취득합니다(2번째의 인수로 지정되는 최대 카운트까지). 이것은 mac_resource_add안에서 이전에 지정된 mrf_arg를 지원합니다(mac_poll의 1번째의 인수로서 지정).

mac_resource_update -

extern void mac_resource_update(mac_t *);

사용 가능한 리소스가 변경되었을 경우에 드라이버로부터 호출됩니다.

mac_rx -

extern void mac_rx(mac_t *, mac_resource_handle_t, mblk_t *);

이 함수는 mblk_t 구조에 포함되는 수신용 패킷의 체인을 전달하기 위해서 호출됩니다. 같은 패킷의 단편(fragment)은 b_cont 필드를 사용해 링크됩니다. 개개의 패킷은 선두의 단편(fragment) 내의 b_next 필드에 의해 링크됩니다. 등록된 리소스가 패킷 체인을 수신하면 적절한 mac_resource_handle_t 값이 함수의 2번째의 인수로서 지정됩니다. 프로토콜 스택은 여러 개의 CPU에 부하를 분산시킬 때 이 값을 힌트로 사용합니다. 같은 흐름에 속하는 패킷은 항상 같은 리소스에 의해서 수신되는 것으로 가정합니다. 리소스가 불명 또는 등록되지 않은 경우는 2번째의 인수로서 NULL을 전달받습니다.

6.2.3 데이터 링크 서비스(DLS) 모듈

DLS 모듈은 DLPI와 유사한 데이터 링크 서비스 인터페이스를 제공합니다. DLPI로 지정되는 STREAMS 메시지 기반의 인터페이스에 대해서 DLS 인터페이스는 커널 수준의 함수 인터페이스입니다. 이 모듈은 상위 레이어가 데이터 링크 서비스를 작성 및 삭제하는데 필요한 인터페이스와 NIC를 플러밍 및 언플러밍하는데 필요한 인터페이스도 제공합니다. GLDv3 기반의 장치 드라이버에 있어서 NIC의 플러밍 및 언플러밍은 이전의 GLDv2 또는 모노리식 DLPI 장치 드라이버 때부터 변경되지 않았습니다. 주요한 변경은 직접 소환, 패킷 체인 및 NIC에 대한 치밀한 제어가 가능한 데이터 경로에 있습니다.

6.2.4 데이터 링크 드라이버(DLD)

데이터 링크 드라이버는 DLS 및 MAC 모듈이 제공하는 인터페이스를 사용하는 DLPI를 지원합니다. 드라이버는 제어 노드에게 건네지는 IOCTL를 사용해 구성됩니다. 이러한 IOCTL가 각각의 DLPI 공급자 노드를 작성 및 삭제합니다. 이 모듈은 NIC를 플러밍/언플러밍하는데 필요한 DLPI 메시지를 취급하며 GLDv3 를 지원하지 않는 클라이언트용으로 STREAMS를 경유하는 데이터 경로에 대해서 하위 호환성을 제공합니다.

6.3 GLDv3 링크 집적 아키텍처

GLDv3 프레임워크는 IEEE 802.3ad로 정의되는 링크 집적을 지원합니다. 이 기능을 설계하는데 있어서 주요한 설계 원칙은 다음과 같습니다.

  • 코드를 변경하지 않고 GLDv3 MAC 드라이버의 집적을 가능하게 한다.
  • 집적되지 않는 장치의 성능을 유지한다.
  • 집적 장치의 성능을 각 구성원의 회선 속도의 누적으로 한다. 예를 들어 집적에 의한 오버헤드를 최소로 한다.
  • 수동 구성과 Link Aggregation Control Protocol(LACP)의 양쪽 모두를 지원한다.

GLDv3의 링크 집적은 aggr로 불리는 의사 드라이버에 의해서 마운트됩니다. 이 드라이버는 링크 집적 그룹에 대응하는 가상 포트를 GLDv3 MAC 레이어에 등록합니다. 그림 4 를 보면 알 수 있듯이 MAC 레이어로 제공되는 클라이언트 인터페이스를 사용하고 집적된 MAC 포트를 제어하여 이 포트와 통신합니다. 또한 dladm 명령어로 사용되는 의사 aggr 멍령어를 export하고 링크 집적 인터페이스를 구성 및 제어합니다. MAC 포트가 링크 집적 그룹의 일부로서 구성되면 DLS 층레이어 등의 다른 MAC 클라이언트가 동시에 액세스 할 수 없게 됩니다. 배타적 액세스는 MAC 레이어에 의해서 적용됩니다. LACP의 마운트는 개별적으로 MAC 포트 또는 링크에 액세스 할 수 있는 aggr 드라이버에 의해서 이루어집니다.

그림 4

GLDv3 aggr 드라이버는 상위 레이어에 대해서 표준의 MAC 모듈로서 동작하고 표준의 NIC 인터페이스로서 표시됩니다. 이것은 dladm가 생성된 후에 ifconfig를 통해 구성 및 관리할 수 있습니다. 각 MAC 포트로부터의 데이터 경로와 인터럽트를 상위 레이어가 개별적으로 관리할 수 있도록, aggr 모듈은 집적을 구성하는 각 MAC 포트를 mac_resource_add 함수를 사용하는 상위 레이어에 등록합니다. 집적 인터페이스는 대부분의 경우 1 개의 IP 주소를 가지는 단일 인터페이스로서 관리됩니다. 데이터 경로는 하나의 CPU/squeue마다 각각의 NIC로서 관리되어 Solaris OS 집적 기능을 실현합니다. 이로써 오버헤드는 제로에 가깝게 되고 집적을 구성하는 MAC 포트의 수에 대하여 선형 확장성이 제공됩니다.

6.4 체크섬 오프로드

Solaris 10 플랫폼에서는 H/W 체크섬 오프로드 기능이 개선되어 거의 어플리케이션으로 전체의 성능이 향상했습니다. 일정 시기에 16 비트의 1의 보수 체크섬 오프로드 프레임워크가 Solaris 소프트웨어에 존재하고 있었습니다. 이것은 처음에 Solaris 2.6 OS의 Zero copy TCP/IP 의 요건으로서 추가되었지만 최근까지 다른 프로토콜을 처리할 수 있도록 확장되지 않았습니다. Solaris 기술에서는 2개 클래스의 체크섬 오프로드를 다음과 같이 정의하고 있습니다.

  • 완전: TCP 및 UDP 패킷의 의사 헤더 체크섬 계산을 포함한 하드웨어 내에서의 완전한 체크섬 계산. 하드웨어가 프로토콜 헤더를 해석할 수 있다는 것이 전제입니다.
  • 부분: 개시, 종료 및 스터프 오프셋에 기반하는 "완전하지 않은" 1 의 보수에 의한 체크섬으로, 하드웨어 내의 의사 헤더 계산 기능을 사용하지 않고 체크섬의 대상이 되는 데이터의 범위와 트랜스포트 체크섬 필드의 장소를 설명합니다.

현재의 대부분의 네트워크 어댑터는 인터페이스에 약간의 차이는 있지만, 어느 것이든 하나의 클래스 체크섬 오프로드를 지원하기 때문에 단편화 되어 있지 않은 IPv4의 케이스(uni-cast 또는 멀티 캐스트)의 지원을 추가하는 것은 송신과 수신의 양쪽 모두에 있어서 중요하지는 않습니다. IPv4/IPv6 상의 TCP/UDP 패킷에 대해 체크섬 계산을 처리할 수 있는 완전 체크섬 네트워크 어댑터는 거의 없기 때문에 IPv6의 경우는 그다지 간단하지는 않습니다.

단편화된 IP의 경우도 이와 같은 제약이 있습니다. 송신 시 체크섬은 단편화되어 있지 않은 데이터 그램에 적용됩니다. 어댑터가 체크섬 오프로드를 지원하기 위해서는 최종적으로 체크섬을 계산하여 회선상에서 단편(fragment)을 송신하기 전에 모든 IP 단편(fragment)을 버퍼링할 수 있어야(또는 하드웨어 내에서 단편화를 실행할 수 있어야) 합니다. 그 때까지 아웃바운드 IP 단편(fragment)의 체크섬 오프로드를 실행할 수 없습니다. 이것에 대해서 대부분의 완전 체크섬(및 모든 부분 체크섬) 네트워크 어댑터는 체크섬 값을 계산하여 그 값을 네트워크 스택에 제공할 수 있기 때문에 수신 단편(fragment)의 리어셈블리는 유연하게 이루어집니다. 단편(fragment)의 리어셈블리의 단계에서 네트워크 스택은 모든 값을 결합함으로써 단편화되어 있지 않은 데이터그램의 체크섬 상태를 요구할 수 있습니다.

IP 옵션이 있는 경우는 체크섬을 오프로드 하지 않게 되어 처리가 간단해졌습니다. 부분 체크섬 오프로드의 경우 일부의 어댑터에서는 개시 오프셋을 단순한 IP 패킷에 충분한 폭에 제한합니다. 옵션이 있기 때문에 프로토콜 헤더의 길이가 이 제한을 넘는 경우 개시 오프셋이 되돌아 오기 때문에 계산이 올바르게 실행되지 않습니다. 완전 체크섬 오프로드의 경우 해당의 어댑터로 IPV4 소스 루팅 옵션을 올바르게 처리할 수 있는 것은 없습니다.

송신의 체크섬 오프로드가 발생하면 네트워크 스택은 해당의 패킷을 드라이버가 체크섬 계산을 하드웨어에 오프로드하는 데 필요한 부속 정보에 연관짓습니다.

인바운드의 경우 드라이버는 하드웨어로 계산되는 체크섬치에 관련지을 수 있는 패킷을 완전하게 제어할 수 있습니다. 드라이버가 DL CAPAB HCKSUM을 통해서 그 기능을 통지하면 네트워크 스택은 IPv4 및 IPv6 패킷의 완전 또는 부분 체크섬 정보를 받아들입니다. 이 프로세스는 단편화되어 있지 않은 유효 하중과 단편화된 유효 하중의 양쪽 모두에 대해 발생합니다.

체크섬 검증은 완전하게 리어셈블리 된 데이터 그램에 대해서 발생하기 때문에 단편화된 패킷은 최초로 리어셈블리 프로세스를 경유할 필요가 있습니다. 리어셈블리 중에 네트워크 스택은 하드웨어로 계산된 각 단편(fragment)의 체크섬 값을 결합합니다.

6.4.. dladm -- 데이터 링크 관리용 새로운 명령어

ifconfig는 스택 내의 다양한 레이어를 관리하기 위해 너무나 많은 기능이 추가되었습니다. Solaris 10 OS에서는 데이터 링크 서비스를 관리하여 ifconfig의 부담을 경감하기 위해 dladm 명령어가 도입되고 있습니다. dladm 명령어는 3종류의 객체에서 작동합니다.

  • link: 이름으로 특정되는 데이터 링크
  • aggr: 키로 특정되는 네트워크 장치의 집적
  • dev: 드라이버명과 인스턴스 번호의 조합으로 특정되는 네트워크 장치

집적의 키는 1~65535의 정수입니다. 장치에 따라서는 구성 가능한 데이터 링크 또는 집적을 지원하지 않습니다. 이러한 장치로 제공되는 고정된 데이터 링크는 dladm을 사용해 표시할 수 있지만 구성할 수는 없습니다.

GLDv3 프레임워크에서 사용자는 집적을 구성하는 한편 집적의 각종 구성원에 걸친 아웃바운드 로드 균형 조정 정책을 선택할 수 있습니다. 정책에서는 패킷 송신에 사용되는 dev 객체를 지정합니다. 정책은 1이상의 레이어 지정자를 콤마(,)로 구분한 목록으로 구성됩니다. 레이어 지정자는 다음 중 하나입니다.

  • L2: 패킷의 발신지 및 송신지 MAC 주소에 따라서 아웃바운드 장치를 선택합니다.
  • L3: 패킷의 발신지 및 송신지 IP 주소에 따라서 아웃바운드 장치를 선택합니다.
  • L4: 패킷에 저장되어 있는 상위 레이어의 프로토콜 정보에 따라서 아웃바운드 장치를 선택합니다. TCP와 UDP의 경우 이것에는 발신지 포트와 송신지 포트가 포함됩니다. IPsec의 경우는 SPI(보안 파라미터 인덱스)가 포함됩니다.

예를 들어 상위 레이어의 프로토콜 정보를 사용하려면 다음의 정책을 사용합니다.

-P L4

발신지 및 송신지 IP 주소에 덧붙여 발신지 및 송신지 MAC 주소를 사용하려면 다음의 정책을 사용합니다.

-P L2,L3

프레임워크는 lacp-modelacp-timer 하위 명령어를 통하여 dladm으로 제어할 수 있는 GLDv3 기반의 집적에 대해서 Link Aggregation Control Protocol(LACP)도 지원합니다. lacp-modeoff , active, 또한 passive로 설정할 수 있습니다.

새로운 장치가 시스템에 삽입되면 재구성 부팅 또는 DR 시에 해당 장치에 대해서 기본값의 비 VLAN 데이터 링크가 작성됩니다. 모든 객체의 구성은 재부팅해도 지속됩니다.

장기적으로 볼 때는 dladm과 모든 정보가 지속적으로 저장되는 전용 파일(/etc/datalink.conf)을 사용하여 장치 고유의 파라미터를 관리할 예정입니다. 현재 이 파라미터는 ndd 드라이버 고유의 구성 파일과 /etc/system에서 관리되고 있습니다.


7.0 성능 조정

Solaris 10 스택의 조정은 사용 중인 하드웨어에 관계없이 뛰어난 혁신적인 성능을 실현하도록 설계되었습니다. 그 비결은 인터럽트 모드와 폴링 모드의 동적인 변환이나 부하가 매우 높은 경우에 폴링 모드로 전환하고 처리량을 높이거나 적절한 대기 시간을 정하는 등의 방법을 사용하는 것입니다. NIC에 의한 패킷 단위의 인터럽트를 허가하는 것으로 부하가 관리 가능한 경우 인터럽트 모드와 폴링 모드의 동적인 변환에 의해 대기 시간이 크게 향상됩니다. 기본값도 하드웨어 구성에 근거하여 신중하게 선택하였습니다. 예를 들어 Solaris 10 이하 릴리스에서는 조정 가능한 변수 tcp_conn_hash_size는 매우 소극적인 값이었습니다. 기본값의 512 해시 패킷은 지원되는 메모리 중에서 최저 구성에 근거하여 선택한 것이었습니다. Solaris 10 OS는 부팅할 때에 빈 메모리를 조사하여 tcp_conn_hash_size의 값을 선택합니다. 같이 시간 대기 상태로부터 접속이 "설정"되면 접속 인스턴스에 관련지을 수 있고 있는 메모리가 곧바로 해방되는 것이 아니라(이 경우도 사용 가능한 총 시스템 메모리에 근거합니다), free_list 으로 설정됩니다. 특정의 시간 내에 새로운 접속이 도착하면 free_list의 메모리 재사용이 시도됩니다. 새로운 접속이 도착하지 않는 경우 free_list는 정기적으로 정리됩니다.

이러한 기능에도 불구하고 경우에 따라서는 조정 가능한 몇 개의 변수를 조정하여 극단적인 경우나 특정 작업의 부하량에 대처할 필요가 있습니다. 다음 섹션에서는 스택의 동작을 제어하는 조정 가능 변수에 대해 설명합니다. 그 영향을 충분히 이해하는 것이 중요합니다. 내용을 충분히 이해하지 못한 상태에서 조작하면 시스템이 불안정해 질 수 있습니다. 대부분의 어플리케이션과 작업 부하량은 기본값에서 최적의 결과를 얻을 수 있다는 것에 유의하십시오.

ip_squeue_fanout: 1개의 NIC로부터의 들어 오는 접속을 모든 CPU에 팬 아웃할지를 제어합니다. 값 0은 들어 오는 접속에 대해 인터럽트를 받은 CPU에 접속하고 있는 squeue에 할당하는 것을 의미합니다. 값 1은 접속에 대해 모든 CPU에 팬 아웃하는 것을 의미합니다. NIC가 CPU보다 고속으로(예를 들어 10Gb NIC) 여러 개의 CPU에서 NIC를 처리해야 하는 경우에는 후자가 필요합니다. /etc/system에서 다음 행을 추가하는 것으로써 설정합니다.

set ip:ip_squeue_fanout=1

ip_squeue_bind: 워커 스레드가 특정 CPU에 바인드할지 여부를 제어합니다. 바인드하고 있는 경우(기본값) 국소성이 향상합니다. 프로세서 설정이 시스템에서 작성되는 경우에만 기본값 이외의 값(바인드 안 함)을 선택해야 합니다. /etc/system에서 다음 행을 추가하는 것으로써 설정을 해제합니다.

set ip:ip_squeue_bind=0

tcp_squeue_wput: 쓰기 측의 squeue 드레인 동작을 제어합니다.

  1. 자체 패킷은 처리하고 squeue는 드레인하지 않는다.
  2. 자체 패킷 및 큐잉되고 있는 패킷을 처리한다.

기본값은 2 이지만 /etc/system에서 다음 행을 추가하는 것으로써 변경할 수 있습니다.

set ip:tcp_squeue_wput=1

CPU의 수가 활성화된 NIC의 수보다 훨씬 많아 플랫폼의 메모리 대기 시간이 기본적으로 길기 때문에 어플리케이션 스레드가 squeue의 드레인을 실시하여 움직일 수 없게 될 가능성이 높은 경우는 이 값을 1로 설정합니다.

ip_squeue_wait: 큐잉된 패킷을 처리할 때까지의 워커 스레드의 대기 시간을 ms 단위로 제어합니다(인터럽트 또는 쓰기 측의 스레드가 패킷을 처리하는 것을 전제로 합니다). 트래픽이 충분한 서버인 경우는 기본값인 10ms가 적절합니다. 그러나 데스크탑과 같이 대화형 트래픽이 많은 시스템에서는 대기 시간이 문제가 되기 때문에 /etc/system에서 다음 행을 추가하는 것으로써 이 값을 0으로 설정할 필요가 있습니다.

ip:ip_squeue_wait=0 

또한 특히 대량의 메모리를 마운트한 시스템에서는 프로토콜 수준의 조정(max_buf, 고위 경계치, 저위 경계치의 변경 등)이 효과적입니다.


8.0 향후의 전망

Solaris 네트워크 스택은 계속해서 레이어간의 수직통합을 기초로 하기 때문에 국소성과 성능면에서 더욱 더 개선될 것이라고 예상됩니다. tip 멀티 스레딩 및 멀티 코어 CPU가 출현함에 따라 저사양 시스템에서도 병렬 실행 파이프 라인의 수가 계속 증가할 것이라고 예상됩니다. 현대의 일반적인 2 CPU 시스템은 듀얼 코어이며 실행 파이프 라인의 수는 4로 대부분의 경우 하이퍼 스레드에도 대응하고 있습니다.

NIC도 성능이 향상되어 MSI-X에 의한 여러 개의 인터럽트, 세세한 분류기능, 여러 개의 DMA 채널, 대규모 세그먼트 오프로드와 같은 각종 Stateless Offload 등을 제공합니다.

향후, TCP 오프로드 엔진, Remote Direct Memory Access(RDMA), iSCSI 의 지원을 포함하여 이러한 하드웨어의 동향을 활용해 나가는 것이 기대됩니다. 현재 다루고 있는 그 밖의 특별한 분야는 다음과 같습니다.

  • 네트워크 스택의 가상화:서버의 통합 및 같은 물리 인스턴스 내에서 여러 개의 가상 시스템을 실행한다고 하는 업계 전체의 동향에 따라 Solaris 스택을 효율적으로 가상화시키는 것이 중요합니다.
  • B/W 리소스 제어: 네트워크의 가상화를 가져온 동향이 동일 선상에서 각종 어플리케이션과 가상 시스템의 대역폭 사용을 효율적으로 제어하는 필요성도 제기하고 있습니다.
  • 고성능의 Sun 이외 모듈의 지원: 현재의 Solaris 10 프레임워크는 여전히 Sun의 모듈 전용입니다. ISV에 있어서는 STREAMS 기반 모듈이 유일한 옵션이기 때문에 새로운 프레임워크의 잠재 능력을 모두 사용할 수 없습니다.
  • 전송 성능: Solaris OS의 전송 성능을 개선하기 위한 작업을 하고 있습니다.
  • 성능이 뛰어난 네트워크 보안: 세계가 복잡해지면서 곳곳에 많은 위험들이 도사리게 되었습니다. 성능과 보안 중 하나만을 선택하라는 것은 이미 불가능해졌습니다. 어느 한쪽을 소홀히 할 수는 없습니다. Solaris 소프트웨어는 항상 매우 강력한 보안을 자랑해 왔지만 Solaris 10 OS는 성능을 희생하는 일 없이 더욱더 강력한 보안을 실현한 점에서 큰 전진을 이루고 있습니다. IP 필터의 성능과 기능 강화 및 서비스 거부 공격의 검출과 그 대처에 관한 완전히 새로운 방법을 제공하기 위해 지속적으로 노력하고 있습니다.

9.0 감사의 말씀

이 기사의 일부를 기고해 준 Thirumalai Srinivasan, Adi Masputra, Nicolas Droux 및 Eric Cheng께 감사의 말씀을 드립니다. Solaris 네트워크 커뮤니티에 계신 모든 회원님들의 아낌없는 지원에도 감사드립니다.


여기서 채택한 모든 기술 자료 내의 코드(기사, FAQ, 샘플 포함)는 특히 다른 라이센스에 대한 언급이 없는 한 이라이센스를 통해 제공됩니다.


BigAdmin