반응형

1. 개요

디바이스 드라이버는 문자, 블록, 네트워크 디바이스 드라이버로 나눌 수 있다. 이 중 네트워크 인터페이스에서 커널과의 상호작용으로 데이터 혹은 패킷을 물리계층을 통해 전송 역할을 하는 것이 네트워크 디바이스 드라이버라고 할 수 있다. 네트워크 디바이스 드라이버의 특징은 리눅스 상의 /dev 디렉터리에 디바이스 파일이 존재하지 않으며 디바이스 드라이버 프로그래밍에서 사용되는 파일 연산 함수인 read(), write() 등의 함수도 사용하지 않는다는 것이다. 또한, 블록 디바이스 드라이버와의 차이점은 블록 드라이버는 커널에서 들어오는 요청만 처리하는 반면, 네트워크 디바이스 드라이버는 외부와 비동기적으로 패킷을 송수신한다는 것이다. 따라서 네트워크 디바이스 드라이버는 주소 설정, 전송을 위한 파라미터 설정, 오류 통계 유지 등의 많은 작업을 지원할 수 있도록 해야 한다. 이를 위해 필요한 것이 네트워크 디바이스 드라이버 프로그래밍이다.

본 문서에서는 네트워크 디바이스 드라이버 프로그래밍을 수행하기에 앞서 기본이 되는 snull 네트워크 디바이스 드라이버 프로그래밍을 수행한다. Snull 네트워크 인터페이스는 가상의 IP 계층 주소 설정만을 이용하여 수행되며, 실질적으로 데이터 링크 계층의 인터페이스를 이용하지 않는다. 그림 1은 snull 프로그래밍을 하기 위한 네트워크 구성을 나타낸 것이다. 그림에서 eth0은 인터넷과 실질적으로 연결된 이더넷 인터페이스이며 sn0, sn1이 가상 인터페이스이다. 그리하여 remote0과 remote1의 두 호스트가 각각 snullnet0과 snullnet1 네트워크에 존재하며 이 호스트 간에 ping 메시지를 통한 데이터가 가상으로(인터페이스를 거치지 않고) 전송되는 것을 확인하는 것이 본 프로그래밍의 목표이다. 표 1은 snull 프로그래밍을 하기 위해 각 인터페이스 및 네트워크, 그리고 호스트에 할당한 IP 주소를 나타낸 것이고, 표 2는 라우팅 테이블을 나타낸 것이다.

 

 

구분

IP 주소

snullnet0

192.168.0.0

snullnet1

192.168.1.0

local0

192.168.0.1

remote0

192.168.0.2

local1

192.168.1.2

remote1

192.168.1.1

목적지

인터페이스

snullnet0

sn0

snullnet1

sn1

 

Snull이 동작하는 원리는 간단하다. 흔히 알고 있는 루프백 주소인 127.0.0.1의 주소와 유사하다. 차이점은 출발지와 목적지가 다른 루프백이라 할 수 있다. 하나의 가상 인터페이스를 통해서 전송된 데이터가 다른 가상 인터페이스를 통해 수신되는 형태이다. 이는 데이터가 전송되는 도중에 출발지와 목적지 주소를 변경함으로써 가능하다. Snull 인터페이스는 하나의 호스트에서 이러한 루프백 효과를 위해 데이터가 전송되는 도중에 출발지와 목적지 주소의 3번째 옥텟의 LSB(Least Significant Bit)를 비트를 뒤집는다. 만약 그림 1에서 remote 1이 remote 0으로 ping을 입력하게 되면 remote 1에서 sn1 인터페이스로 ping request 메시지가 전송되고, sn0 인터페이스에서 remote0 호스트로 ping response 메시지가 전송된다. 즉, ping request 메시지의 출발지 주소는 192.168.1.1, 목적지 주소는 192.168.1.2가 되고, ping response 메시지의 출발지 주소는 192.168.0.1, 목적지 주소는 192.168.0.2가 되어 전송된다.

 

2. Snull 구현

2.1. 관련 자료구조

2.1.1. net_device

net_device 구조체는 네트워크 디바이스 드라이버에서 핵심이 되는 자료구조이며 이 구조체는 /include/linux/netdevice.h 파일에 정의되어 있다. net_device 구조체는 전역 정보, 하드웨어 정보, 인터페이스 정보, 유틸리티 필드, 통계 정보 등 매우 다양한 필드들로 구성되어 있다. 이 중 대표적인 필드에 대해 표 3을 통해 설명한다.

 

필드

내용

char name[IFNAMSIZ]

네트워크 디바이스의 인터페이스 이름

unsigned long rmem_end

공유된 받기를 위한 메모리의 끝주소

unsigned long rmem_start

공유된 받기를 위한 메모리의 시작주소

unsigned long mem_end

공유된 메모리의 끝주소

unsigned long mem_start

공유된 메모리의 시작주소

unsigned long base_addr

디바이스의 I/O를 위한 주소

unsigned int_irq

디바이스에 할당된 IRQ 번호

unsigned char if_port

인터페이스를 위한 포트 타입(BNC,AUI,TP 등)

unsigned char dma

DMA 채널 번호

unsigned long state

디바이스의 현재 상태

struce net_device *next

네트워크 디바이스들의 연결 리스트

int (*int)(struct net_device *dev)

네트워크 디바이스의 초기화 함수 포인터

int ifindex

인터페이스의 index

int iflink

인터페이스의 link

struct net_device_stats *(*get_stats) (struct net_device *dev)

디바이스 통계정보를 알려주는 함수 포인터

struct iw_statistics *(*get_wireless_stats) (struct net_device *dev)

디바이스 무선통계정보 알려주는 함수 포인터

unsigned long trans_start

마지막으로 송신한 시점으로 jiffies 값을 가짐

unsigned long last_rx

마지막으로 수신한 시점을 나타냄

unsigned short flags

인터페이스의 flag를 나타냄

unsigned short type

인터페이스 하드웨어 타입

unsigned short hard_header_len

하드웨어 헤더의 길이

void *priv

디바이스 드라이버의 private data 포인터

struct net_device *master

해당 디바이스가 속한 그룹의 mastet 디바이스의 포인터

unsigned char broadcast [MAX_ADDR_LEN]

하드웨어 브로드캐스트 주소

unsigned char dev_addr [MAX_ADDR_LEN]

하드웨어 주소

unsigned char addr_len

하드웨어 주소의 길이

int allmulti

모든 멀티캐스트되는 패킷을 다받음

in watchdog_timeo

watch dog timer의 timeout 설정

 

2.1.2. sk_buff

sk_buff는 /linux/skbuff.h 파일에 정의되어 있으며 소켓에서 사용되는 데이터들은 이 소켓 버퍼에 저장되며 리눅스에서 제공되는 프로토콜 스택의 처리를 받고 네트워크 디바이스 드라이버에 도달하게 된다. 소켓 버퍼에서 사용되는 필드의 내용을 보면 표 4와 같고 사용하는 함수는 표 5와 같다.

 

필드

내용

struct net_device *dev

소켓 버퍼가 전달되거나 보내지는 디바이스를 나타냄

union {/* .... */} h;

union {/* .... */} nh;

union {/* .... */} mac;

패킷 내부에 포함된 전송층 헤더

패킷 내부에 포함된 네트워크 계층 헤더

패킷 내부에 포함된 링크 계층 헤더

unsigned char *head

unsigned char *data

unsigned char *tail

unsigned char *end

네트워크를 통해서 전달되는 패킷의 데이터를 나타냄

unsigned long len

데이터 자체의 길이를 나타내는 것을 skb->tail, skb->head를 가짐

 

필드

내용

struct sk_buff *alloc_skb(unsigned int len, int priority)

소켓 버퍼를 할당

struct sk_buff *dev_alloc_skb(unsigned int len)

소켓 버퍼를 할당하며 priority를 GFP_ATOMIC으로 설정하고, head와 tail 사이에 16바이트를 남겨둠

void kfree_skb(struct sk_buff *skb, int rw)

소켓 버퍼를 해지

void dev_kfree_skb(struct sk_buff *skb, int rw)

디바이스 드라이버에서 소켓 버퍼를 해지

unsigned char *skb_put(struct sk_buff *skb, int len)

버퍼의 끝에 데이터를 넣고 tail과 len field를 변경

int skb_tailroom(struct sk_buff *skb)

소켓 버퍼에 남은 데이터를 위한 영역 크기를 리턴

int skb_headroom(struct sk_buff *skb)

데이터 앞에 남은 부분의 사용가능한 영역 크기 리턴

int skb_reserve(struct sk_buff *skb, int len)

소켓 버퍼에 데이터를 쓰기 전에 headroom에 공간을 마련함

unsigned char *skb_pull(struct sk_buff *skb, int len)

패킷의 head로부터 데이터를 분리함

 

2.2. 등록/해제

네트워크 디바이스 드라이버 모듈을 커널에 등록하기위해서 드라이버는 자원을 요청하고 기능을 제공한다. 네트워크 디바이스 드라이버의 많은 부분이 문자, 블록과 다른 형태의 함수블록을 많이 채용한 것과 달리 등록과 해제과정에 별다른 과정은 없다. 다만 디바이스 드라이버와 하드웨어 위치를 탐색하지 않고 등록하기만 한다는 점이다. 또한 네트워크 인터페이스에는 주 번호와 부 번호의 개념이 없기 때문에 네트워크 드라이버는 이런 번호를 요청하지 않으며 대신 새로 감지한 인터페이스를 네트워크 디바이스 전역 목록의 자료 구조로 집어넣는다.

 

2.2.1. alloc_netdev()

snull은 snull의 상태를 나타하기위해 netdevice.h에 정의된 구조체를 사용하며, alloc_netdev()를 통해 모듈에 얹을 때 할당 받는다. 여기서 sizeof_priv는 드라이버의 “개인적 자료”영역의 크기를 설정해준다. name은 인터페이스의 이름, 그리고 마지막 setup은 구조체의 나머지 부분을 호출하기 위한 초기화함수를 가리키는 포인터를 말한다. snull 함수 구현에 있어서 개인적 자료 영역의 크기는 struct snull_priv라는 구조체의 크기를 받아와 잡아주었고, name은 sn%d로 넘버를 부여 받을 수 있도록 해주었고, setup은 명령어를 담고 있는 snull_init로 해주었다.

struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *);

 

2.2.2. snull_init()

각 디바이스 드라이버를 초기화 시켜주는 함수이다. 이 초기화 작업은 register_netdev 호출 전에 완료되어야한다. net_device 구조체는 사실 아주 크고 복잡한데, 다행이도 커널이 ether_setup()를 통해 이더넷 관련된 몇몇 필드를 자동으로 설정해 준다. 이 함수에서는 ether_setup()에 dev 파라메터를 넣어 몇몇 필드의 설정을 받아오오고 dev->open, dev->stop, dev->set_config, dev->hard_start_init, dev->do_ioctl, dev->get_stats, dev->rebuild_header, dev->tx_timeout, dev->wathdog_timeo, dev->flags, dev->features, dev->hard_header_cache 등을 설정해 줌으로서 초기화를 완료한다.

void snull_init(void);

 

2.2.3. snull_clean_up()

모듈 적재를 해제할 때는 별다른 작업은 필요하지 않으며, 단순히 인터페이스 등록을 해제(unregister_netdev)하고 내부적으로 필요한 작업 정리(snull_teardown_pool)를 수행한 후 net_device 구조체를 시스템으로 반납(free_netdev)한다. 또한 module_exit()에 link되어 모듈을 내릴 때 적용된다.

void snull_cleanup(void);

 

2.2.4. snull_init_module()

이 함수는 시작하는 함수인 module_init()로부터 처음 호출되는 함수로 메모리할당 작업과 모듈을 올리는데 있어서 오류 사항 등을 점검해준다. allow_netdev 함수로 실제 사용될 구조체인 snull_devs[0], snull_devs[1]에 할당하는 작업을 수행한다고 보면 된다.

void snull_init_module(void);

 

2.2.5. snull_open()

인터페이스가 패킷을 실어 나르기 전에 커널은 인터페이스를 열어서 주소를 대입해야 한다. 커널은 ifconfig 명령어에 반응해서 인터페이스를 열고 닫는다. open은 필요한 시스템 자원을 요청하고 인터페이스가 시작하도록 요청한다. 실제 하드웨어가 없는 상황에서는 open 함수가 할 일이 그리 많지는 않다. MAC주소는 인터페이스가 외부 세상과 통신하기에 앞서 하드웨어 디바이스에서 dev->dev_addr로 복사를 해주는 과정이 필요하다. 또 netif_start_queue()를 설정해 줌으로서 인터페이스에 필요한 큐를 할당함으로써 패킷을 전송할 수 있는 상태가 된다.

int snull_open(struct net_device *dev);

 

2.2.6. snull_close()

open과 반대로 stop하기 위해서는 인터페이스를 종료하고 시스템 자원을 해제한다. 해제하는 부분에서는 큐를 멈춰주는 netif_stop_queue()만 설정해주면 모든 설정이 끝나게 된다. 이 함수는 netif_start_queue와 반대로 작용한다. 다시 말해 디바이스가 더 이상 패킷을 전송할 수 없는 상태로 만들어주는 것이다.

int snull_release(struct net_device *dev);

 

2.3. 송신

리눅스 커널이 다루는 각 패킷은 소켓 버퍼 구조체인 sk_buff에 들어 있다. sk_buff는 상위 네트워크 계층으로부터 넘겨받으며 일종의 패킷이라고 봐도 무방하다. 이 sk_buff를 가리키는 포인터는 일반적으로 skb라고 부르기도 한다. 이 skb->data에 실제 전송할 패킷이 들어가며, skb->len은 옥텟 단위로 길이를 나타낸다. 리눅스 커널이 이러한 sk_buff를 전송하기 위해서는 hard_start_transmit이라는 함수를 호출하여 패킷을 queue에 집어넣는다. hard_start_xmit에 전달하는 소켓 버퍼는 전송 계층의 헤더를 포함한 물리적인 패킷을 포함한다. 따라서 인터페이스는 전송할 자료를 변경할 필요가 없다.

 

2.3.1. snull_tx()

snull_tx()는 skb의 최소 크기 검사 등 패킷에 대한 기본적인 점검을 수행하고, 소켓 버퍼 구조체로부터 패킷 데이터(skb->data)와 길이(skb->len) 값을 하드웨어 연관 함수인 snull_hw_tx 함수로 넘겨주는 역할을 한다. 실질적으로 네트워크 디바이스 드라이버를 구현할 시에는 특정 랜카드 특성에 맞게 이를 구현해야 하며, 본 snull에서는 가상 인터페이스로 존재하기 때문에 snull_hw_tx 함수를 이용하여 구현한다. snull_tx()의 기본형은 다음과 같으며, 리턴값은 성공할 경우 0, 실패할 경우 음수의 값으로 설정된다.

int snull_tx(struct sk_buff *skb, struct net_device *dev);

data = skb->data; // 데이터 저장

len = skb->len; // 데이터 길이 저장

dev->trans_start = jiffies; // timestamp 저장

snull_hw_tx(data, len, dev); // 하드웨어 함수에 데이터, 데이터 길이값, 디바이스 포인터 값 넘김

 

2.3.2. snull_hw_tx()

snull_hw_tx 함수는 하드웨어 관련 전송 함수이며 snull은 가상 네트워크 인터페이스로 설정하기에 이를 함수로서 구현한다. 실질적으로 네트워크 디바이스 드라이버를 구현할 시에는 하드웨어 특성에 맞게 프로그래밍되어야 하며 DMA(direct memory access) 등을 사용하여 구현된다. 본 snull에서는 가상으로 두 개의 인터페이스에서 패킷을 송수신하는 일종의 편법을 이용하여 구현한다. 이를 위해 전송하는 ping 패킷의 출발지 주소와 목적지 주소의 세 번째 옥텟값의 LSB를 반전시켜 서로 다른 호스트로부터 패킷이 전송되는 것과 같은 방법을 이용한다. snull_hw_tx의 기본형은 다음과 같다.

void snull_hw_tx(char *buf, int len, struct net_device *dev);

if (len < sizeof(struct ethhdr) + sizeof(struct iphdr)) {//송신될 패킷에 이더넷 헤더와 IP 헤더 있는지 확인

printk("snull: Hmm... packet too short (%i octets)\n", len);

return;

}

ih = (struct iphdr *)(buf+sizeof(struct ethhdr)); //출발지주소와 목적지주소 설정

saddr = &ih->saddr;

daddr = &ih->daddr;

((u8 *)saddr)[2] ^= 1; /* 인터페이스 하나를 통해 보내지는 패킷을 다른 인터페이스가 수신할 수 있도록 출발지 및 목적지 주소를 변경. 3번째 옥텟의 LSB를 반전시킴 */

((u8 *)daddr)[2] ^= 1;

2.3.3. snull_tx_timeout()

디바이스 드라이버를 설계하는 데 있어 타이머를 통한 타임아웃 설정은 상당히 중요하다고 할 수 있다. 이와 관련하여 net_device 구조체의 watchdog_timeo 필드가 존재한다. 그리하여 시스템의 시각이 디바이스로부터 전송된 패킷의 trans_start 시각에 타임아웃 시간을 더한 값을 넘어서게 되면 네트워크 계층에서는 드라이버의 snull_tx_timeout을 호출하게 된다. 그래서 타임아웃이 발생하게 되면 통계에 오류를 표시하기 위해 ‘stats.tx_error++;’을 실행하고, 이후 전송 큐를 다시 시작하게 된다. 기본형은 다음과 같으며 본 snull 디바이스 드라이버에서는 사용되지 않는다.

void snull_tx_timeout (struct net_device *dev);

 

2.3.4. 기타

hard_start_xmit()는 net_device 구조체에 있는 xmit_lock이 동시 호출을 막는 역할을 한다. 이 함수가 반환됨과 동시에 이 함수는 다시 호출될 수 있다. 즉, 하드웨어 전송이 아직 완료되지 않았음에도 불구하고 새로운 패킷을 전송하려는 경우가 존재한다. 이러한 사항은 가상으로 패킷을 주고받은 본 snull 프로그래밍과는 무관하지만 추후 이더넷 혹은 무선랜 인터페이스를 이용할 시에는 고려해야만 한다.

실제로 하드웨어는 패킷을 비동기적으로 전송하고 나가는 패킷을 저장하기 위해 활용 가능한 메모리 총량에 제약이 있다. 때문에 드라이버는 하드웨어가 새로운 자료를 받아들이도록 준비할 때까지 전송을 시작하지 말도록 요청할 필요가 있다. 이는 netif_stop_queue(struct net_device *dev)를 호출함으로써 가능한데, 이 함수를 통해 큐를 정지시켰다면 드라이버는 전송을 위한 패킷을 받아들일 준비가 끝난 미래 어느 시점까지 큐를 다시 시작할 수 있도록 해야 한다. 이때는 netif_wake_queue(struct net_device *dev)를 호출함으로써 가능하다. 이 함수는 netif_start_queue와 같은 기능을 하며 단지 패킷 전송을 다시 시작하기에 앞서 네트워크 시스템과의 상호작용이 있다는 점이다.

 

2.4. 수신

Snull에서의 패킷 수신부의 구현은 하드웨어를 통해 받은 패킷을 컴퓨터의 메모리에 올려주고 snull_rx를 호출하여 사용한다. 네트워크 드라이버가 구현하는 패킷 수신은 인터럽트 방식과 폴링 방식 두 가지로 나뉜다. 인터럽트 방식은 대다수의 드라이버가 구현하는 방식으로 제어하는 하드웨어 장치가 서비스를 받아야 할 때 인터럽트를 발생시킨다. 예를 들어 이더넷 디바이스 드라이버는 네트워크에서 이더넷 패킷을 받을 때마다 인터럽트를 발생하게 된다. 폴링 방식은 고대역폭 어댑터를 위한 드라이버가 구현하는 방식으로 시스템 타이머를 이용하여 호출하며 요청한 명령이 수행되었는지 검사하게 된다.

 

2.4.1. snull_rx()

아래의 코드는 snull "인터럽트" 처리기에서 호출하는 함수부로 어떤 네트워크 드라이버에도 적용시킬 수 있는 일반적인 형태로 구현되었다. 소켓 버퍼가 현재 동작하고 있는 네트워크 디바이스를 가리키는 포인터와 패킷 길이를 나타내는 두 개의 인자가 필요하다.

 

• 버퍼 할당

패킷을 담을 버퍼를 할당하는 작업으로 버퍼 할당 함수인 dev_alloc_skb(lenghth)가 사용된다. include/linux/skbuff.h 헤더를 갖으며 alloc_skb() 함수를 사용하여 소켓 버퍼를 형성하며 자료 길이를 인자로 갖는다. 여기에서 datalen+2를 해주는 이유는 14비트인 이더넷 헤더를 16비트로 정렬해주기 위함이다. printk_ratelimit() 함수를 사용하여 커널 메시지를 지나치게 많이 출력하여 시스템에 동작에 영향을 미치는 것을 막는다. 초 단위로 속도제한을 설정하며 default 값으로 0을 갖는다.

skb = dev_alloc_skb(pck -> datalen +2);

if (!skb) {

if(printk_ratelimit())

printk(KERN_NOTICE "snull rx : low on mem - packet dropped\n");

priv -> stats.rx_dropped++;

goto out;

}

 

• 버퍼에 패킷 데이터 복사

skb를 할당 받았다면 memcpy(memlen1, memlen2, n) 함수를 사용하여 버퍼에 패킷 자료를 복사한다. 이 함수는 메모리 영역 memlen1을 메모리 영역 memlen2로 n바이트 복사하겠다는 의미이며 반환 값은 memlen1이 된다. 또한, 여기에 skb_put(skb, len)함수가 쓰이는데 포인터 skb를 len만큼 증가시킨다는 의미로 이 함수를 호출하기 전에 tailroom(빈 공간)이 충분한지 검사하여야 한다.

memcpy(skb_put(skb, pkt -> datalen), pkt -> data, pkt -> datalen);

 

• dev와 protocol 필드 값 세팅

패킷을 감지해내기 위하여 사용하는 두 필드로 이더넷 지원 코드는 ethenet_type_trans를 사용한다.

skb -> dev = dev;

skb -> protocol = eth_type_trans(skb, dev);

 

• checksum 관리

네트워크 드라이버에는 세 가지의 checksum이 존재한다. CHECKSUM_KW는 이미 하드웨어에서 checksnum을 수행하였다는 의미이다. CHECKSUM_NONE는 체크섬을 아직 하지 않았고 소프트웨어로 checksnum을 관리하겠다는 의미이다. default 값으로 쓰인다. CHECKSUM_UNNECESSARY는 checksum 작업을 수행하지 않는다는 의미이다.

skb -> ip_summed = CHEKSUM_UNNECESSARY;

 

• 통계 카운터 갱신

패킷을 받았음을 기록하기 위하여 통계 카운터 갱신 과정을 거친다. 중요한 필드는 rx_packets, rx_bytes, tx_packets, tx_bytes로써 각각 받은 패킷, 받은 패킷의 옥텟 개수, 보낸 패킷, 보낸 패킷의 옥텟 개수를 나타낸다.

priv -> stats.rx_packets++;

priv -> stats.rx_bytes += pkt -> datalen;

 

• 소켓 버퍼 상위 전달

패킷 수신의 마지막 단계로 netif_rx(skb)함수를 사용하여 네트워크 계층으로 패킷을 전송한다.

netif_rx(skb);

 

2.5. 인터럽트 처리기

네트워크 인터페이스는 오류나 연결 상태 변화 등을 신호하기 위해 인터럽트를 생성한다.

 

• 포인터 인출

처리기가 제일 먼저 수행하는 작업으로 dev_id를 인수로 받는다. 이 부분은 등록된 디바이스의 하드웨어를 점검하여 원하는 내용을 가리키고 있는지 확인하는 부분으로 인수로 받는 dev_id의 포인터에서 포인터를 얻는다.

1 struct net_device *dev = (struct net_device *)dev_id;

 

• 디바이스 잠금과 해제

<linux/spinlock.h>에 포함되어 있는 함수로 spin_lock(&mr_lock) 함수를 사용하여 디바이스 잠금을 실행한다. spin_lock은 critical region을 보존하기 위하여 사용하는 방법으로 일정기간동안 디바이스를 잠그는 것으로 스핀락을 기다리는 동안에는 인터럽트가 불가능하다. 스핀락을 호출하면 다음 스핀락을 얻을 때까지 스핀을 계속하게 된다. 그리고 spin_unlock(&mr_lock)함수를 통하여 디바이스 잠금을 해제해 준다. 본 인터럽트 처리기에서는 디바이스 잠금을 해제함과 동시에 작업이 끝나게 된다.

1 priv = netdev_priv(dev);

2 spin_lock(&priv -> lock);

3 spin_unlock(&priv -> lock);

 

• 인터럽트 처리

네트워크에서 인터럽트는 패킷이 왔을 때, 패킷 전송이 완료되었을 때 발생하며 각각은 랜카드의 status register를 확인함을 통하여 구분할 수 있다. 만약 패킷이 왔다면 snull_rx를 호출하여 처리하게 되며 패킷 전송이 완료된 것이면 skb에 할당된 메모리를 해제한다.

 

1 statusword = priv -> status;;

2 priv -> status = 0;

3 if (statusword & SNULL_RX_INTR) {

4 pkt = priv -> rx_queue;

5 if (pkt) {

6 priv -> rx_queque = pkt_next;

7 snull_rx(dev, pkt);

8 }

9 }

 

• skb에 할당된 메모리 해제

패킷 전송을 마친 인터럽트라면 skb에 할당된 메모리를 해제한다. 이때 dev_kfree_skb(*skb) 함수를 호출하는데 이는 전송완료 상태를 다루는 부분이다. 이는 통계를 갱신하고 사용하지 않을 소켓 버퍼를 시스템에 반환하기 위하여 사용한다. 호출할 수 있는 함수는 아래의 세 가지가 있다. dev_kree_skb(struct sk_buff *skb)는 코드가 인터럽트 문맥에서 작동하지 않음을 알 경우에 호출하는 것으로 snull은 실제 하드웨어 인터럽트가 없기 때문에 이 함수를 사용한다. dev_kfree_skb_irq(struct sk_buff *skb)는 인터럽트 처리기에서 버퍼를 해제할 것을 알고 있을 때 사용한다. dev_kfree_skb_any(struct sk_buff *skb)는 관련 코드가 인터럽트나 인터럽트가 문맥인 곳에서 동작할 수 있을 때 사용한다.

 

1 if(statusword & SNULL_TX_INTR) {

2 priv -> stats.tx_packets++;

3 priv -> stats.tx_bytes += priv -> tx_packetlen;

4 dev_kfree_skb(priv -> stb);

5 }

 

2.6. 수신 인터럽트 완화

네트워크 디바이스를 인터럽트 방식에 의해 사용하게 되면 인터페이스가 패킷을 수신할 때 마다 인터럽트에 걸리는 문제가 발생한다. 전달해야 하는 패킷의 양이 많지 않을 때는 문제가 되지 않지만 초당 수 천 개의 패킷을 받을 수 있는 인터페이스에서 인터럽트 부하가 걸리게 되면 전반적인 시스템 성능에 문제를 일으킬 수 있다.

이에 따라 인터럽트를 완화하기 위하여 개발된 방식이 폴링을 기반으로 한 NAPI(new API)이다. 이는 기존의 인터럽트 처리 함수에서 데이터를 처리할 때 폴링을 처리하는 것이다.

 

• NAPI 모드 작동

use_napi 매개 변수에 0이 아닌 어떠한 값을 넣고 snull 드라이버를 적재하면 NAPI 모드가 동작한다. 반드시 poll 필드와 weight 필드를 설정해주어야 하는데 poll 필드는 드라이버의 폴링함수로 설정해야 하는데 폴링함수란 ...이다. weight 필드는 인터페이스에서 받아들일 소통량을 말하는 것으로 인터페이스가 받아들일 수 있는 소통량보다 큰 값으로 지정해서는 안 된다.

1 if(use_napi) {

2 dev -> poll

=snull_poll;

3 dev -> weight

 

• 인터럽트 처리기 교체

커널에게 앞에서 설명한 인터럽트를 사용하지 않고 인터럽트 폴링을 시작할 시점이라고 커널에게 알려주어야 한다. 인터럽트 처리기에서의 코드 변경이 필요하다.

1 if(statusword & SNULL_RX_INTR) {

2 snull_rx_ints(dev, 0);

3 netif_rx_schedule(dev);

4 }

 

• poll 함수 선언

4.2에서 사용한 netif_rx_schedule 함수를 사용하여 poll 메소드를 호출한다.

1 int(*poll) (struct net_device *dev, int *budget);

 

• snull_rx와의 차이점

snull_rx와 아래의 몇 가지 사항에서 차이가 있다.

• budget과 quota : snull_rx에서 사용한 pkt 인자 대신 최대 패킷 개수를 커널에 전달하도록 허용하는 budget이 사용된다. 반면에 quota는 초기화 시점에서 인터페이스에 할당한 weight로 시작하는 인터페이스마다 존재하게 된다.

• netif_receive_skb : snul_rx에서 사용하였던 netif_rx를 netif_receive_skb로 대신하여 커널에 제공한다.

• netif_rx_complete : poll 메소드가 주어진 시간 내에 주어진 패킷을 모두 처리한다면 수신 인터럽트를 다시 활성화 시키고 netif_rx_complete를 호출하여 폴링을 끄고 0을 반환한다. 만약에 1을 반환하게 되면 앞으로 처리해야할 패킷이 남아있음을 의미한다.

 

3. 실행 화면

3.1. 설정 확인

커널에 snull 모듈을 적재하기에 앞서 대조군이 될 현재의 설정을 살펴본다. 인터페이스 할당과 커널에 적재된 모듈이 있는지 살펴보는 것이다. ifconfig 명령어를 통하여 snull 네트워크 디바이스 드라이버가 할당되지 않았음을 확인한다. ifconfig는 윈도우상의 ipconfig와 유사한 명령어로 인터페이스 설정 상태와 IP 설정 상태를 확인할 수 있다. ifconfig로 인터페이스 상태를 확인하는 그림2와 같다. eth0은 인터넷과 연결된 이더넷 인터페이스이고 lo는 루프백주소로 각각에 할당된 하드웨어 주소와 IP 주소를 확인할 수 있다.

 

그리고 lsmod 명령어로 커널에 적재된 모듈을 확인한다. lsmod는 적재된 모듈의 리스트를 확인하는 명령어로 명령어 뒤에 옵션을 달아 사용할 수도 있다. 아직 snull 모듈 적재 작업을 수행하지 않았으므로 list에 우리가 사용하려는 Snull 모듈은 적재되어 있지 않다

 

3.2. 모듈 컴파일 및 적재

기존의 설정 확인을 마치면 실제로 모듈을 커널에 적재하기위한 작업이 필요하다. 여기서부터는 root 권한으로 접속하여 진행이 되어야한다. snull코드와 makefile 등이 포함된 snull의 기본 소스를 ~/Snull 디렉토리에 넣은 후 모듈 컴파일 및 적재 작업을 시작한다. ls 명령어는 지정한 경로의 파일 및 디렉토리를 확인하는 명령어로 이용하여 Snull 디렉토리 내의 파일을 확인할 수 있으며 make 명령어와 디렉토리에 존재하는 makefile을 이용하여 모듈을 적재할 수 있다. makefile은 컴파일과 링크를 실행하는 파일로써 make라는 명령어에 의해 실행된다. make는 이에 해당하는 설정파일이다. make 명령어를 실행시키면 build과정을 거쳐 그림 4와 같이 모듈이 생성됨을 확인할 수 있다.

 

 

명령을 실행 후 다시 ls 명령어를 통해 Snull 디렉토리에 오브젝트 파일이 생성됨을 확인할 수 있다. 오브젝트 파일은 ‘.ko’ 또는 '.o'의 확장자명을 갖으며 이 오브젝트 파일을 가지고 모듈 적재를 할 수 있다.

 

 

모듈을 적재하는 명령어는 insmod로써 모듈을 삽입하겠다는 의미이고 사용 규칙은 “insmod 오브젝트파일”이다. ‘snull.ko’오브젝트 파일의 적재를 하기위해서는 그림 6과 같이 “insmod snull.ko”로 쓸수 있다. 모듈이 제대로 적재되어 있는지는 앞에서 사용하였던 lsmod 명령어를 통하여 확인할 수 있다. 그림 3과 다르게 snull 모듈이 적재되어 있음을 확인할 수 있다.

 

 

ifconfig 명령어를 통하여 다시 한 번 인터페이스 상태를 확인한다. ifconfig -a를 사용하게 되면 모든 인터페이스의 상태를 확인하는 것으로 우리가 사용하게 될 가상 인터페이스 sn0과 sn1의 상태도 확인할 수 있다. sn0과 sn1 모두에 하드웨어 주소와 ip주소가 할당되지 않았음을 확인할 수 있다. pan0은 브리지를 만들어 ip를 부여하고 가상 인터페이스를 연결시킨다.

 

 

3.3. 하드웨어 주소 및 네트워크와 호스트 설정

위의 3.2절에서 sn0과 sn1에 하드웨어 주소가 설정되지 않았음을 확인하였다. 하드웨어 주소를 설정은 각각 sn0과 sn1에 해주며 규칙에 어긋나지 않게 임의의 주소를 생성하면 된다. sn0과 sn1의 주소 값이 겹치지 않게 설정하도록 한다. 나중에 default 값으로 하드웨어 주소가 들어감으로 임의의 값을 지정해주면 된다. 그리고 sn0과 sn1을 up해주는데 up은 각 네트워크 인터페이스가 생성됨을 의미한다.

 

 

3.2절에서 ifconfig 명령어를 사용하였을 때 IP 주소가 표시되지 않음은 네트워크와 호스트 번호를 설정하지 않았기 때문이다. /etc의 /networks 디렉토리에서 네트워크 번호를 할당해 줄 수 있다. 본 프로그래밍에서는 ip 충돌을 방지하기 위하여 사설 주소를 사용하여 설정하였다. vi는 유닉스 기반의 텍스트 편집기로 이 명령어를 사용하지 않고 바로 디렉토리로 접속하여 파일 내용을 변경하려고 하면 변경된 내용이 올바로 저장되지 않는다. 그림 10은 그림 9의 명령어를 통하여 파일의 내용을 변경한 것으로 각각 snullnet0과 snullnet1의 네트워크를 설정한다.

 

 

 

네트워크 설정을 마쳤다면 호스트 설정을 시작한다. 그림 1을 참고하여 호스트 설정 작업을 진행한다면 훨씬 수월할 것이다. 호스트 설정 작업은 /etc/hosts에서 수행하며 네트워크 설정 작업과 마찬가지로 vi를 사용한다. 호스트는 local0, local1, remote0, remote1 4가지를 설정해주며, local0의 호스트 부분이 remote1과 동일하며 local1의 호스트 부분이 remote0과 동일하다는 것이다.

 

 

만약 컴퓨터가 네트워크에 이미 연결되어 있다면 선택한 숫자가 이미 사용 중에 있을 수 있다. 이러한 상황에서 인터페이스에 사용 중인 네트워크 값을 대입하였다면 인터페이스가 실제 호스트와 통신을 가로막을 수 있다. 그래서 선택한 숫자와 무관하게 그림 13의 명령어를 사용하면 인터페이스를 올바르게 설정할 수 있다. 이 명령어를 사용하지 않으면 ifconfig -a를 통해 인터페이스 상태를 확인했을 때 ip 주소가 제대로 설정되지 않았을 것이다.

 

위의 설정을 모두 마쳤다면 ifconfig -a 명령어를 통하여 다시 한 번 인터페이스 상태를 확인한다. sn0과 sn1에 각각 하드웨어 주소와 ip 주소가 제대로 설정되어 있음을 확인할 수 있다. 이제 호스트 간에 데이터 전송을 확인하기 위한 설정은 모두 마친 것이다.

 

 

3.4. 핑 테스트

각 호스트 간의 핑 테스트를 통하여 네트워크 드라이버가 제대로 설정되어 있는지 확인해 본다. 본 문서에서는 두 가지 방법으로 핑 테스트를 사용했는데 첫 번째 방법은 직접 호스트의 주소를 입력하여 데이터의 전송을 확인하는 것이고 두 번째 방법은 호스트가 remote0과 remote1에 도달하는 것을 확인하는 것이다. 그림 15는 첫 번째 방법에 해당하는 것으로 <ping IP주소>의 입력형태를 갖는다. 각각의 호스트로 ICMP 메시지를 통해 제대로 동작되고 있음을 확인할 수 있다.

 

 

다음 그림 16은 두 번째 방법을 통한 핑 테스트이다. <ping -c num remote>의 입력형태를 갖게 되는데 num은 몇 개의 패킷을 전송할 것인지 입력하는 것이고 remote는 각각의 remote0 또는 1을 적어주면 된다. ping statics를 통하여 패킷 전송이 어떻게 이루어졌는지 확인할 수 있는데 앞에서 입력 값으로 2를 받았기 때문에 패킷은 두 개가 전송되었을 것이고 전송 패킷 중 몇 개의 전송이 성공했는지 실패했는지 확인할 수 있다. 또한 패킷 전송에 대한 최소, 평균, 최대속도와 지연율을 확인할 수 있다.

 

 

4. 결론

본 문서에서는 네트워크 디바이스 드라이버 중 가장 기본적인 드라이버인 snull 드라이버에 대한 분석을 마쳤다. snull 드라이버는 가상의 인터페이스를 잡아주는 드라이버였다. 코드를 분석하고 동작을 확인함으로써 완료된 것을 볼 수 있다. 목표인 유선인터페이스와 무선인터페이스를 동시에 가지는 라우터를 만들기 위한 기초 작업으로 할당, 해제, 수신, 송신과 같은 기능들을 함수로 구현해 본 것이다. 앞으로 snull 드라이버에서 구현해 보았던 기능들을 기반으로 하여 앞으로 802.3 device driver를 진행하게 될 것이다.

참 고 문 헌

[1] 조나단 코벳 외 2명, “리눅스 디바이스 드라이버,” 한빛미디어, 2007.

반응형

'리눅스' 카테고리의 다른 글

vi, vim 명령어  (0) 2010.05.31
네트워크 디바이스 드라이버2  (0) 2010.05.18
디바이스 드라이버 인터럽트 처리  (0) 2010.05.18
gcc 다운그레이드  (0) 2010.05.01
gcc 다운그레이드(downgrade) 하기 - fedora, ubuntu  (0) 2009.10.15

+ Recent posts