'디바이스드라이버'에 해당되는 글 4건

  1. 2010.05.26 네트워크 서브 시스템
Network2010. 5. 26. 17:05
반응형

네트워크 서브 시스템

네트워크 서브 시스템은 리눅스가 지금처럼 널리 확산되는데 많은 공헌을 했으며, 리눅스의 최대 장점 중의 하나로 인식되고 있는 분야이다. 이처럼 중요한 위치를 차지하고 있음에도 지금껏 리눅스 커널의 네트워크 서브 시스템의 구조를 분석하고 이해하려는 시도가 많이 부족한 것이 사실이다.

이번 글에서는 리눅스의 최대 장점 중 하나로 꼽히는 네트워킹 부분에 대한 구현을 살펴보겠다. 네트워크 코드는 너무나 방대한 영역이기 때문에 한 번에 살펴보는 것이 불가능하므로 아주 단순한 소켓 프로그램을 예제로 하여 기본적인 소켓의 생성, 연결, 데이터 전송/수신 과정에 대해 살펴보기로 한다. 네트워크는 또한 보안에 민감한 영역이기 때문에 곳곳에 보안을 위한 코드들이 포함되어 있음을 확인할 수 있을 것이다.


자료 구조


소켓 버퍼 - sk_buff 구조체
소켓 버퍼는 네트워크로 전송되는 패킷을 나타내는 자료 구조로서, 네트워크 서브 시스템 전반에서 사용되는 중요한 구조체이다. 소켓 버퍼를 정의한 sk_buff 구조체는 <include/linux/skbuff.h>에 정의되어 있다.

next, prev, list는 소켓 버퍼를 관리하기 위한 포인터이다. 소켓 버퍼를 저장하는 큐는 sk_buff_head 구조체의 형태로 각 소켓 버퍼를 이중 연결 리스트로 관리한다. sk는 소켓 버퍼가 속한 소켓을 나타내며, stamp는 패킷을 받은 시간을 저장한다. net_device 구조체의 dev, input_dev, real_dev 필드는 현재 패킷을 받거나 보내기 위한 네트워크 장치를 가리키는 변수이다. 다음으로 나오는 3개의 union 필드들은 각각 OSI 7 계층의 전송 계층, 네트워크 계층, 데이터 링크 계층의 헤더 정보를 저장한다.

이들 헤더 정보들은 데이터 영역 내에 순서대로 저장되어 있으며, 각각의 계층을 지나면서 해당 계층의 프로토콜에 맞는 헤더 정보를 적절히 설정한다. dst 필드는 패킷을 전송하기 위한 정보를 저장하는 구조체이다. cb는 각 프로토콜에서 사용되는 제어 정보들을 저장하는 역할을 하는 버퍼이다. truesize 필드는 sk_buff 구조체 자체의 크기에 데이터 영역의 크기를 더한 실제 소켓 버퍼 구조체의 크기를 나타낸다.

소켓 버퍼 내의 데이터에 접근하기 위한 필드로 head, data, tail, end가 있다. 이 중 head와 end는 처음에 할당한 데이터 영역의 시작과 끝을 가리키는 고정된 필드이다. data와 tail은 그 중에서 실제로 데이터가 저장된 영역의 시작과 끝을 가리키는 필드로 소켓 버퍼로 데이터가 추가될 때마다 변경된다. 소켓 버퍼의 내용은 실제 데이터 앞에 각 계층 별로 헤더 정보가 추가되는 형태이므로 데이터 영역의 처음부터 사용할 수 없기 때문에 이러한 필드를 이용하여 쉽게 접근할 수 있도록 한다.

그리고 실제 구조체에는 포함되어 있지 않지만 소켓 버퍼의 데이터를 관리하기 위해 데이터 영역의 뒷부분에 추가적으로 struct skb_shared_info 구조체가 사용된다. 이 구조체는 데이터 영역을 참조하고 있는 소켓 버퍼의 수, fragment를 이루는 소켓 버퍼 정보 등을 포함한다. 소켓 버퍼의 대략적인 형태는 <그림 1>과 같이 나타낼 수 있다.

<그림 1> 소켓 버퍼


소켓 버퍼를 다루기 위한 여러 함수들이 존재한다. 먼저 소켓 버퍼를 할당하기 위해 alloc_skb 함수가 사용된다. 이 함수는 주어진 크기만큼의 데이터 영역을 가지는 소켓 버퍼를 생성한다. 또한 디바이스 드라이버에서 패킷을 수신했을 때 소켓 버퍼를 생성하기 위해 사용하는 dev_alloc_skb 함수가 있다. 이 함수는 헤더 정보를 포함하기 위해 주어진 크기보다 16바이트 만큼을 더하여 소켓 버퍼를 생성하고, skb_reserve 함수로 16바이트 만큼을 예약해 둔다. 생성된 소켓 버퍼는 kfree_skb 함수를 통해 해제된다.

또한 소켓 버퍼를 복사하기 위한 skb_copy, skb_clone 함수와 데이터 영역을 가리키는 포인터를 조작하기 위한 skb_put, skb_push, skb_pull 등의 함수가 있다. 그리고 소켓 버퍼를 큐에 넣거나 빼는 일을 수행하는 skb_queue_tail, skb_dequeue, skb_insert, skb_append, skb_unlink 등의 함수도 제공한다.

네트워크 장치 - net_device 구조체
net_device 구조체는 리눅스 커널 내에서 네트워크 장치를 표현하기 위해 사용하는 구조체이다. 네트워크 장치는 일반 블럭 장치나 문자 장치와는 달리 /dev 디렉토리 내에 특정한 장치 파일을 가지지 않으며, 단순한 read/write 연산만으로는 접근할 수 없으므로 일반 장치와는 달리 취급된다. net_device 구조체는 I/O 연산에 필요한 하드웨어 정보 뿐 아니라 이를 관리하기 위한 고수준의 자료 구조 및 함수에 대한 정보를 포함하는 거대한 구조체로 네트워크 서브 시스템 전반에 걸쳐 사용된다. net_device 구조체는 <include/linux/netdevice.h>에 정의되어 있다.

먼저 net_device 구조체의 앞쪽에 나오는 하드웨어 정보를 살펴보기로 한다. name은 네트워크 장치가 가질 이름을 저장한다. 이더넷 장치의 이름은 특별히 지정하지 않는 한 <net/ethernet/eth.c>에 정의된 alloc_etherdev와 netif_queue_stopped 함수에 의해 부여된다. 이후 dev_get_by_index 등의 함수로 장치의 레퍼런스를 얻어오는 것이 가능하다. iflink 필드는 패킷을 전송할 네트워크 장치의 인덱스를 저장하는 변수로 기본적으로 ifindex 필드와 같은 값을 가지지만 터널링 장치와 같은 경우에는 실제로 패킷을 전송할 다른 장치의 인덱스 값을 가지게 된다. get_stats 필드는 장치의 통계 정보를 얻기 위한 함수의 포인터를 저장한다.

mtu 필드는 장치가 전송할 수 있는 최대 패킷 크기 정보를 저장하며, type 필드는 하드웨어의 종류에 대한 정보를 저장한다. hard_header_len 필드는 데이터 링크 계층에서 필요한 헤더 정보의 길이를 나타내며, priv 필드는 하드웨어의 종류에 따라 특정한 정보를 저장할 목적으로 사용된다. <net/ethernet/eth.c>에 정의된 ether_setup;
int;
int ;
...
}


다음은 디바이스 드라이버 영역에서 사용될 고수준의 정보들이다. 먼저 상위의 프로토콜에 따른 정보들을 저장하기 위한 포인터 변수들을 각각 유지한다.

qdisc 필드는 장치에서 패킷 정보를 저장할 큐에 대한 정보를 나타낸다. 패킷을 전송하는 경우 장치가 큐를 지원한다면 장치의 qdisc가 가리키는 큐에 소켓 버퍼 데이터를 저장해 두었다가 나중에 처리하고 그렇지 않다면 바로 전송한다. tx_queue_len 필드는 큐에 저장될 수 있는 최대 소켓 버퍼의 수를 나타낸다. 그리고 상위 계층에서 장치에 대한 연산을 수행하기 위해 호출되는 함수들의 포인터를 저장한다.

간단한 소켓 프로그래밍 예제
다음은 단순한 에코 클라이언트 프로그램으로 W. Richard Stevens의 『Unix Network Programming』이라는 책의 1장에 나오는 예제를 약간 수정한 것이다. 간략한 설명을 위해 대부분의 에러 처리 부분은 생략했고 write 부분을 추가했다. 이 프로그램을 실행시킨다면 서버에 hello라는 문자열을 전송한 뒤 똑같이 hello라는 문자열을 서버로부터 받게 될 것이다. 다음의 예제에서 주의 깊게 봐야 할 함수는 socket, connect, write, read의 네 가지이다. 이들 각각에 대해 커널 내부에서 어떤 일이 일어나는지 살펴보자.


#include unp.h

int main
{
int sockfd;
char line;
struct sockaddr_in servaddr;

if
err_quit;

sockfd = socket;

bzero);
servaddr.sin_family = AF_INET;
servaddr.sin_port = ntons;
inet_pton;

connect &servadr, sizeof);

write;
read;
recvline = 0; /* null terminate */
fputs;

exit;
}


소켓의 생성과 연결


socket 함수가 호출된다. 이 함수는 <net/socket.c>에 정의되어 있으며 sock_create 함수를 이용해 파일 디스크립터에 연결한 뒤 이 값을 리턴한다. sock_create 함수를 호출하며 이 함수가 실제 소켓을 생성하는 일을 수행한다.


static int __sock_create
{
int i;
int err;
struct socket *sock;

if
return -EAFNOSUPPORT;
if
return -EINVAL;

if {
static int warned;
if {
warned = 1;
printk\n, current->comm);
}
family = PF_PACKET;
}

err = security_socket_create;
if
return err;

#if defined
if
{
request_module;
}
#endif

net_family_read_lock {
i = -EAFNOSUPPORT;
goto out;
}


먼저 인자로 주어진 family와 type 변수가 올바른 값인지를 검사한다. 앞의 예제의 경우라면 PF_INET과 SOCK_STREAM이 넘어오게 된다. PF_INET의 PF는 ‘Protocol Family’를 의미하며 AF에 해당하는 값과 동일하다. 리눅스에서 지원하는 프토토콜의 목록은 <include/linux/socket.h>에 정의되어 있다. 그리고 호환성을 위해 PF_INET에 대하여 SOCK_PACKET 타입을 명시한 경우 family 값을 PF_PACKET으로 수정한다. 그리고 security_socket_create 함수로 단순히 0을 리턴한다. 이후에 net_families 변수가 저장하고 있는 등록된 프로토콜의 배열에서 주어진 family가 존재하는지 검사한다. 만약 커널 모듈을 지원하는 경우라면 request_module))
{
printk;
i = -ENFILE;/* Not exactly a match, but its the
closest posix thing */
goto out;
}

sock->type = type;

i = -EAFNOSUPPORT;
if )
goto out_release;

if ) < 0)
goto out_module_put;

if ) {
sock->ops = NULL;
goto out_module_put;
}

module_put;
*res = sock;
security_socket_post_create;

out:
net_family_read_unlock;
out_release:
sock_release;
goto out;
}


그리고는 sock_alloc를 생성한다. 생성된 소켓의 타입에 인자로 주어진 type 변수를 설정하고 try_module_get에 맞는 net_families 구조체의 멤버인 create를 생성한다.

앞의 경우 inet_family_ops 구조체의 create 함수가 호출된다. 커널에서는 이렇게 생성된 INET 소켓을 사용하여 필요한 작업을 처리하지만 사용자 레벨에서는 BSD 소켓 인터페이스를 사용하여 프로그래밍이 이루어진다. 그리고는 try_module_get 함수를 호출하여 보안 사항을 점검한 뒤 net_family 구조체에 대한 락을 해제하고 리턴한다.

connect 부분은 소켓을 통해 통신할 상대방 측과의 신뢰성 있는 연결을 확립하는 과정이다. 먼저 conect에서 연결을 요청하기 위해 SYN이라는 형태의 패킷을 상대방에게 보낸다. SYN 패킷을 받은 서버는 이에 대한 확인을 위해 SYN-ACK 패킷을 보내고, 마지막으로 클라이언트가 이에 대한 응답으로 ACK 패킷을 서버에게 보냄으로써 연결이 성립되는 형태이다. 이렇게 연결 요청시 총 3단계로 패킷을 주고받기 때문에 3-way handshake라고 한다.

<그림 2> 3-way handshake in TCP


connect 함수에서 처리된다. 먼저 인자로 주어진 파일 디스크립터를 통해 해당 BSD 소켓의 정보를 얻어온 후 소켓에 연관된 연산자 구조체의 connect 시스템 콜을 호출할 때 PF_INET, SOCK_STREAM으로 설정했으므로 이 과정에서 <net/ipv4/af_inet.c>에 정의된 inet_stream_ops 구조체의 inet_stream_connect 함수를 다시 호출하고 타임아웃에 관련된 처리를 한 후 state를 SS_CONNECTED 상태로 변경한다.

TCP 프로토콜에서 처리하는 connect 함수는 tcp_prot 구조체의 tcp_v4_connect 시스템 콜의 인자로 주어진 소켓 주소에 대해 ip_route_connect 함수를 호출하여 클라이언트 측의 포트를 자동으로 할당한다. 이 값은 sysctl_local_port_range, sysctl_local_port_range 사이의 값으로 할당 가능하며, 이전에 할당된 값이 tcp_port_rover 변수에 저장되어 있으므로 이 값보다 1만큼 더 큰 값에서부터 검색을 시작한다. 이렇게 포트가 할당되면 ip_route_newports 함수를 호출하여 SYN 패킷을 위한 소켓 버퍼를 생성하고 tcp_transmit_skb에 의해 tcp_rcv_synsent_state_process 값을 동기화하고 sk_state 필드를 TCP_ESTABLISHED 상태로 변경한 후 ACK 패킷을 서버로 전송한다.


패킷의 전송


응용 계층 - Echo client
응용 프로그램에서 네트워크로 데이터를 전송하기 위해서는 생성된 소켓에 write, send, sendto, sendmsg 등의 시스템 콜을 사용할 수 있다. 여기서는 가장 일반적인 형태인 write 연산에 대해 살펴보도록 하겠다. write 시스템 콜을 호출하면 커널의 sys_write 형식을 따라 주어진 파일에 맞는 연산을 처리할 수 있도록 vfs_write 함수는 do_sync_write 함수를 호출하도록 되어 있으므로 결국 sock_aio_write 함수는 적절한 인자를 설정한 후 __sock_sendmsg 함수가 호출되고, 이 함수는 다시 __sock_sendmsg
{
struct sock_iocb *si = kiocb_to_siocb;
int err;

si->sock = sock;
si->scm = NULL;
si->msg = msg;
si->size = size;

err = security_socket_sendmsg;
if
return err;

return sock->ops->sendmsg;
}


__sock_sendmsg 시스템 콜이 호출될 때 주어진 사용자 공간의 데이터인 hello를 가리키며 size 인자는 5가 된다. 이 함수는 소켓 I/O 연산에 필요한 sock_iocb 구조체를 적절히 설정한 뒤 security_socket_sendmsg 함수가 호출된다. inet_sendmsg. 우리는 SOCK_STREAM 인자를 주어 소켓을 생성했기 때문에 이 과정에서 최종적으로 TCP 프로토콜의 sendmsg 처리 함수인 tcp_sendmsg 함수는 <net/ipv4/tcp.c>에 정의되어 있다. 먼저 소켓에 대한 락을 획득하고 msg에 대한 플래그가 있다면 설정한다. TCP_CHECK_TIMER 함수는 패킷 전송시 대기할 시간을 MSG_DONTWAIT 플래그가 설정된 경우 0으로 그렇지 않다면 sk_sndtimeo 값으로 설정한다.

sk_sndtimeo 값은 setsockopt 값을 가지며 실제적으로 거의 무한정 기다리게 된다. 그리고 현재 소켓의 상태가 연결이 확립된 상태가 아니라면 sk_stream_wait_connect 함수를 호출하여 패킷 헤더 부분을 제외한 실제 데이터 영역의 크기를 계산한다.

이제 실제 데이터 전송에 필요한 정보를 설정하는데 hello라는 문자열 하나의 데이터만을 가지고 있으므로 iovlen = 1, iov->iov_base = hello, iov->iov_len = 5로 설정되어 있을 것이다. copied는 실제 전송된 데이터의 양을 나타내는 변수로 처음에는 0으로 설정한다. 그리고 현재까지 실행되는 동안 에러가 발생됐는지 소켓이 닫혔는지를 검사하여 이 경우 적절한 처리를 하고 전송을 종료한다.


while {
int seglen = iov->iov_len;
unsigned char __user *from = iov->iov_base;

iov++;

while {
int copy;

skb = sk->sk_write_queue.prev;

if <= 0) {

new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/
if )
goto wait_for_sndbuf;

skb = sk_stream_alloc_pskb,
0, sk->sk_allocation);
if
goto wait_for_memory;

/*
* Check whether we can use HW checksum.
*/
if )
skb->ip_summed = CHECKSUM_HW;

skb_entail;
copy = mss_now;
}


전송은 각각의 iov에 대하여 일어나므로 우리의 경우는 한번만 처리될 것이다. 현재 iov에 대하여 데이터의 길이와 포인터를 각각 seglen, from 변수에 저장한 후에 iov 포인터를 증가시킨다. seglen=5이므로 while문 안으로 들어와서 skb 포인터를 소켓의 전송 큐 내에 있는 마지막 소켓 버퍼를 가리키도록 설정한다. 소켓이 최초에 생성되면 sk_send_head 필드가 NULL로 설정되므로 if문 안쪽의 new_segment 부분으로 들어가서 새로운 소켓 버퍼를 생성한다.

sk_stream_memory_free에 공간이 남아있는지 검사한 후 sk_stream_alloc_pskb 함수를 이용하여 전송 일련번호를 설정한 후에 소켓 구조체의 전송 큐에 넣어진다.


/* Where to copy to

*/
if > 0) {
if )
copy = skb_tailroom;
if ) != 0)
goto do_fault;
} else {
int merge = 0;
int i = skb_shinfo->nr_frags;
struct page *page = TCP_PAGE;
int off = TCP_OFF;

if &&
off != PAGE_SIZE) {
merge = 1;
} else if )) {
tcp_mark_push;
goto new_segment;
} else if {
off = &
~;
if {
put_page;
TCP_PAGE = page = NULL;
}
}

if {
/* Allocate new cache page. */
if ))
goto wait_for_memory;
off = 0;
}

if
copy = PAGE_SIZE - off;

err = skb_copy_to_page;
if {
if ) {
TCP_PAGE = page;
TCP_OFF = 0;
}
goto do_error;
}

/* Update the skb. */
if {
skb_shinfo->frags.size +=
copy;
} else {
skb_fill_page_desc;
if ) {
get_page;
} else if {
get_page;
TCP_PAGE = page;
}
}

TCP_OFF = off + copy;
}


다음으로 소켓 버퍼의 공간이 남아있다면 이 공간에 skb_add_data 함수를 호출하여 소켓의 전송 메시지를 위한 페이지 내에 복사할 수 있는지 검사하고, 그렇지 않고 네트워크 장치가 Scatter-Gather I/O를 지원하지 않거나 이미 MAX_SKB_FRAGS 만큼의 단편화가 이뤄졌다면 new_segment 부분으로 돌아가서 새로운 소켓 버퍼를 생성한다. 만일 이미 페이지가 꽉 차 있다면 페이지를 해제하고 새로운 페이지를 할당받아 skb_copy_to_page
TCP_SKB_CB->flags &= ~TCPCB_FLAG_PSH;

tp->write_seq += copy;
TCP_SKB_CB->end_seq += copy;
skb_shinfo->tso_segs = 0;

from += copy;
copied += copy;
if == 0 && iovlen == 0)
goto out;

if )
continue;

if ) {
tcp_mark_push;
__tcp_push_pending_frames;
} else if
tcp_push_one;
continue;
...
out:
if
tcp_push;
TCP_CHECK_TIMER;
release_sock;
return copied;


copied 변수가 0이라면 TCP 헤더의 PSH 플래그를 지우고 전송 일련번호를 갱신한 뒤 from과 copied 변수도 복사된 만큼 증가시킨다. 첫 번째 if문의 조건을 만족하므로 out 부분으로 이동한 뒤 tcp_push 함수는 __tcp_push_pending_frames 함수를 호출한다. tcp_write_xmit에 대해 tcp_snd_test 함수를 호출한다. 이 함수는 소켓의 TCP 연산을 나타내는 tcp_func 구조체의 queue_xmit 필드가 가리키는 함수를 호출하는 데 이 함수는 IP 계층의 ip_queue_xmit 함수는 <net/ipv4/ip_output.c>에 정의되어 있다. 이 함수는 크게 두 부분으로 나눌 수 있는데 먼저 앞부분은 커널의 라우팅 테이블을 검색하여 패킷이 전송될 목적지의 주소를 알아내는 일이다. 먼저 해당 소켓으로 이미 다른 패킷을 보내서 목적지에 대한 캐시 데이터를 가지고 있다면 이 과정을 생략한다. 그리고 __sk_dst_check 정보 등이 저장된다. 이렇게 생성한 정보를 가지고 ip_route_output_flow 함수를 이용하여 옵션 정보를 생성한다. 그리고 ip_select_ident_more 함수에서 checksum 값을 계산한다.


마지막으로 소켓 버퍼의 우선순위를 설정한 후 NF_IP_LOCAL_OUT이라는 Netfilter Hook으로 넘겨 패킷을 필터링할지를 검사한 다음에 NF_HOOK 함수를 호출한다. dst_output 함수를 처리하는 과정에서 ip_output 함수는 우선 패킷의 전송이 요청됐음을 나타내는 통계 정보를 증가시킨다. 소켓 버퍼의 데이터의 길이를 검사하여 현재 목적지로 보낼 수 있는 최대 전송 크기보다 큰 경우에는 ip_fragment 함수를 호출하여 그대로 패킷을 전송한다. 우리의 경우 데이터의 길이는 5이므로 ip_finish_output
goto no_route;

/* OK, we know where to send it, allocate and build IP header. */
iph = skb_push + );
*iph) = htons | | );
iph->tot_len = htons;
if && !ipfragok)
iph->frag_off = htons;
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl;
iph->protocol = sk->sk_protocol;
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
skb->nh.iph = iph;
/* Transport layer set skb->h.foo itself. */

if {
iph->ihl += opt->optlen >> 2;
ip_options_build;
}

ip_select_ident_more->tso_segs);

/* Add an IP checksum. */
ip_send_check;

skb->priority = sk->sk_priority;

return NF_HOOK;

no_route:
IP_INC_STATS;
kfree_skb;
return -EHOSTUNREACH;
}


ip_finish_output 함수를 호출한다. ip_finish_output2가 들어갈 만한 공간이 있는지 검사하여 없는 경우 skb_realloc_headroom 함수를 호출하여 필요한 메시지를 출력한다.

그리고 목적지에 대한 하드웨어 헤더 캐시 정보를 가지고 있다면 캐시에 포함된 헤더 정보를 소켓 버퍼에 저장하고, 캐시의 hh_output 필드가 가리키는 함수를 호출한다. 캐시 정보를 가지고 있지 않다면 직접 다음 번 전송될 목적지에 대한 output 필드가 가리키는 함수를 호출한다. output 필드는 neigh_resolve_output 함수를 가리킨다. 이제 dev_queue_xmit 함수는 IP 계층에서 처리된 소켓 버퍼를 실제 네트워크 장치에게로 넘겨 전송하는 역할을 하며 <net/core/dev.c>에 정의되어 있다.

우선 소켓 버퍼가 소켓의 데이터 전송용 페이지 내에 fragment로 나눠져 있다. 하지만 전송할 네트워크 장치에서 fragment 혹은 SG I/O를 지원하지 않거나, 하나 이상의 fragment가 장치에서 DMA로 접근할 수 없는 영역에 있다면 __skb_linearize 함수를 이용하여 계산하고 local_bh_disable {
spin_lock;

rc = q->enqueue;

qdisc_run;

spin_unlock;
rc = rc == NET_XMIT_BYPASS

NET_XMIT_SUCCESS : rc;
goto out;
}

if {
int cpu = smp_processor_id {

HARD_TX_LOCK;

if ) {
if
dev_queue_xmit_nit;

rc = 0;
if ) {
HARD_TX_UNLOCK;
goto out;
}
}
HARD_TX_UNLOCK;
if )
printk;
} else {
if )
printk;
}
}

rc = -ENETDOWN;
local_bh_enable;
return rc;
out:
local_bh_enable, 이 큐에 소켓 버퍼를 집어넣고 qdisc_run 함수는 다시 qdisc_restart). 여기서 전송할 수 있다면 dev 구조체의 hard_start_xmit 필드가 가리키는 함수를 호출하여 네트워크 장치에게 넘기고, 그렇지 않다면 다시 큐에 넣고 netif_schedule 함수를 이용하여 장치가 패킷을 전송할 수 있는지 검사한 후 역시 hard_start_xmit 필드가 가리키는 함수를 호출한다. 이 함수는 각 장치의 드라이버 내에 위치하고 있으며 이에 대한 일반적인 형태로 <drivers/net/isa-skeleton.c> 파일 내의 net_send_packet 함수를 참조하기 바란다.

패킷의 수신
이제 네트워크 장치를 통해 받은 패킷이 처리되는 과정에 대해 살펴보자.

데이터 링크 계층 - pci-skeleton
네트워크 장치가 패킷을 수신하면 인터럽트가 발생한다. 이 인터럽트에 대한 처리는 각 장치에 따라 다르므로 여기서는 <drivers/net/pci-skeleton.c> 파일에서 구현한 PCI 버스를 사용하는 일반적인 네트워크 장치에 대한 부분을 살펴볼 것이다. 먼저 이 장치의 open
{
...

retval = request_irq ;
if {
DPRINTK ;
return retval;
}

...
}


장치의 irq 번호에 해당하는 인터럽트에 대해 netdrv_interrupt과 수신에 해당하는 인터럽트 처리 함수를 호출하는데 여기서는 netdrv_rx_interrupt 함수를 이용하여 소켓 버퍼를 생성하고, eth_copy_and_sum 함수를 호출하여 하드웨어 헤더 정보를 설정하고 이더넷 프로토콜 정보를 리턴한다. 그리고 netif_rx 함수가 맡고 있다.

이 함수는 현재 CPU의 softnet_data 구조체의 poll_list에 대하여 처리를 한다. 그 자료 구조에 접근하는 동안에는 인터럽트로 인해 새로운 패킷이 추가되지 않도록 인터럽트를 금지시켜야 한다. 장치가 처리할 수 있는 양을 넘었거나 너무 많은 시간이 흐른 경우에는 다음 번 softirq 시점에서 처리하도록 softnet_break 부분으로 이동하여 리턴하고, 그렇지 않다면 dev 구조체의 poll 필드가 가리키는 함수를 호출한다. 이 함수는 process_backlog 함수를 호출한다. 이 함수는 하드웨어 헤더 정보를 읽어 적절한 처리를 한 후 패킷의 프로토콜에 해당하는 packet_type 구조체의 처리 함수를 호출한다. 이 경우 ip_packet_type 구조체의 ip_rcv한다. 그리고 pskb_may_pull 규정에 따라 다음 4가지 사항을 점검한다.

① 패킷의 길이가 IP 헤더 정보의 길이보다 작지는 않은가

②IP 버전의 4인가

③checksum이 올바른가

④패킷의 길이 정보가 올바른가

여기서 IP 헤더에 포함된 헤더 길이 정보는 4의 배수의 형태로 기록되어 있으므로 실제 길이와 비교하기 위해서는 4를 곱하는 형태가 되어야 한다. 혹은 IP 헤더의 최소 길이는 20바이트이므로 이를 검사하기 위해서는 ihl 필드가 5보다 작은지 검사할 수도 있다. 이 단계를 거친 올바른 패킷이라면 NF_IP_PRE_ROUTING Netfilter Hook을 거쳐 ip_rcv_finish 함수는 먼저 ip_route_input 함수를 가리키도록 설정된다. 그렇지 않고 자신을 거쳐 다른 호스트에게 보내지는 패킷의 경우에는 ip_forward ip_options_complie 함수를 호출하여 dst 구조체의 input 필드가 가리키는 ip_local_deliver 함수는 패킷이 fragment라면 ip_defrag 함수를 호출한다. 이 함수는 먼저 IP 헤더 길이만큼 데이터를 이동시켜 TCP 헤더 정보로 설정한 뒤 해당 프로토콜에 해당하는 정보를 찾아 상위 프로토콜로 넘겨주는 일을 한다. 이 경우 tcp_protocol 구조체의 tcp_v4_rcv 함수는 주어진 TCP 헤더 정보에 따라 적절한 처리를 한 뒤 소켓 버퍼의 출발지와 목적지의 네트워크 주소 및 포트 번호를 통해 __tcp_v4_lookup 함수를 통해 필터링을 하고 tcp_v4_do_rcv tcp_rcv_established tcp_v4_hnd_req 시스템 콜에 대한 부분에서 간략히 살펴본 대로 tcp_rcv_state_process 함수는 ACK에 대한 처리와 타임스탬프, 수신 일련번호 및 윈도우에 대한 처리를 한 후에 사용자 공간으로 데이터를 복사해 준다.

이 때 softirq를 처리하는 프로세스가 소켓을 기다리는 프로세스라면 현재 프로세스의 상태를 TASK_RUNNING으로 만들고 tcp_copy_to_iovec 함수로 설정되어 있으며, sk 구조체의 sk_sleep 필드가 가리키는 wait_queue에서 잠들어 있는 프로세스들을 깨운다.

응용 계층
응용 프로그램에서 read의 경우와 마찬가지로 sys_read→do_sync_read 함수를 거쳐 __sock_recvmsg 함수를 호출하여 보안 사항을 점검하고 실제 루틴인 BSD 소켓 구조체의 ops 구조체의 recvmsg 필드가 가리키는 함수를 호출한다. inet_stream_ops 구조체의 recvmsg 필드는 sock_common_recvmsg에 해당한다.
이 함수는 루프를 돌며 sk_receive_queue 내의 소켓 버퍼를 검사하여 원하는 offset 에 해당하는 소켓 버퍼의 데이터를 찾아 복사한다. 이 과정에서 프로세스가 시그널을 받는다면 sock_rcvtimeo 함수를 호출하여 timeo 시간만큼 기다린다. 이 때 프로세스는 INET 소켓 구조체의 sk_sleep 필드가 가리키는 wait_queue에서 잠든다. 이 후에 패킷을 받으면 tcp_rcv_established 기능이 포함되면 패킷의 전송 과정에서 다음과 같은 5가지의 Netfilter Hook을 거치는 데 각각의 역할은 다음과 같다.


◆ NF_IP_PRE_ROUTING : 네트워크 장치로부터 수신된 모든 패킷을 처리한다. 실제로 패킷에 대한 처리가 이루어지기 전에 필터링이 가능하게 되므로 DOS 공격에 대한 처리나 목적지 네트워크 주소 변환의 처리, 통계 정보 기록 등을 하기에 알맞다.
◆ NF_IP_LOCAL_IN : 로컬 머신에게 전송된 패킷만을 처리한다.
◆ NF_IP_FORWARD : 로컬 머신을 통해 다른 머신에게로 forwarding되는 패킷만을 처리한다.
◆ NF_IP_LOCAL_OUT : 로컬 머신에서 송신하는 패킷만을 처리한다.
◆ NF_IP_POST_ROUTING : 네트워크 장치를 통해 송신하는 모든 패킷을 처리한다. 출발지 네트워크 주소 변환이나 masquerading의 처리, 통계 정보 기록 등을 하기에 알맞다.


<그림 4> 네트워크 계층


전송 계층 - TCP
<그림 5>는 전송 계층의 패킷 처리 과정을 보여준다. TCP 계층에서 패킷을 수신하면 현재 소켓의 상태에 따라 각기 다른 함수가 호출된다. 소켓이 연결된 상태의 처리 함수인 tcp_rcv_established()는 소켓 버퍼의 데이터를 사용자 공간의 버퍼에 복사하며 이를 기다리며 잠든 프로세스가 있다면 깨운다. 송신 과정에서는 소켓 버퍼를 생성하여 데이터를 복사하고 모든 계층의 헤더 정보가 들어갈 만한 공간을 확보해 둔다.

<그림 5> 전송 계층
반응형
Posted by pmj0403