Tuesday, June 24, 2008

Utility functions


struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype; /* AF_INET */
int h_length; /* h_addr_list length */
char **h_addr_list;
}

위 hostent의 h_addr_list는 network byte order로 된 in_addr인데, h_addr_list는 char ** 이므로, 이를 실제로 사용할 때는 inet_ntoa(*(int *)h_addr_list[i]) 식으로 하면 4byte를 읽어 inet_ntoa에 넣게 되므로 값을 확인해볼 수 있다. 아래는 각 함수별 설명이다.

gethostbyname(...) 은 호스트 이름으로 hostent 를 가지고 오는 함수이다.

gethostbyaddr(...) 는 IP주소로 hostent를 가지고 오는 함수이다

getsockopt(...), setsockopt(...) 는 소켓 옵션 관련 함수이고, 앞에서 했고,..

getsockname(...) 은 bind된 sockaddr 정보를 얻어오는 함수이다 (자동 할당된 포트 번호 확인 시 사용)

getpeername(...) 은 반대로 연결된 상대편의 sockaddr정보를 얻어오는 함수이다

getservent(), getservbyname(), getservbyport() 함수들은 /etc/services파일을 가져오는 함수들이다(전체 다 가져오거나, 서비스 이름으로 찾거나, 포트 번호로 찾는 함수)


getprotoent(), getprotobyname(), getprotobynumber() 역시 마찬가지로 /etc/protocols 파일을 가져오는 함수들이다.


위 두그룹들은 각각 serverent, protoent 구조체를 알아야 한다.


주소변환 함수 그룹들은 지난번에 기록한것 같은데 이름만 다시 정리하면, inet_addr/inet_aton, inet_pton, inet_ntoa, inet_ntop 이다. pton, aton의 차이는 리턴되는 값을 static공간에 담아주냐 아니냐의 문제인데, reentrant를 고려하여 제공되는 함수가 pton, ntop 군이니 필요할 때 구별해서 쓰면된다.

Socket, options

소켓 사용시 여러 옵션을 줄 수 있는데, 아래 함수를 통해 set, get이 가능하다.

int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);

설정할 수 있는 옵션은 영역이 매우 다양한데(이를 테면, TCP에서의 옵션이라던가, IP레이어에서의 옵션이라던가 등...), 어느 영역에서의 옵션을 설정할 것인지를 두번째 인자인 level에서 선택할 수 있다. 몇개의 옵션들은 아래와 같다.
  • SOL_SOCKET
    • SO_BROADCAST
    • SO_REUSEADDR
      • TCP의 TIME_WAIT상태의 소켓을 다시 재사용할 수 있게 해주는 옵션으로 서버쪽에서 active close해서 TIME_WAIT상태로 빠지면 이 시간동안 listen이 실패하므로, 바로 listen할 수 있도록 TIME_WAIT상태에서소 소켓을 사용할 수 있도록 하기 위해 사용한다.
    • SO_LINGER
      • struct linger { int l_onoff; int l_linger; } 를 사용한다.
      • 데이터를 전송 후 끊기 위해 기다리는 시간을 l_linger에 지정할수 있는데, 0인경우 버퍼를 없에버리고 바로 close하며 client 쪽에도 RST를 보내서 TIME_WAIT되지 않고 바로 끊도록 한다. (aborty shutdown, aborty close. 0이 아닌 경우라면 graceful close/shutdown)
    • SO_KEEPALIVE
      • 일정시간마다 연결상태 확인
    • SO_OOBINLINE
    • SO_RCVBUF
      • 수신 버퍼크기 조정(실제로는 지정한 값의 두배가 잡힘). 자동으로 조정되는 경우에는 설정할 수 없는 경우도 있음
    • SO_SNDBUF
    • SO_RCVTIMEO
      • blocking 함수를 사용하는 경우(recv, ...) block에서 깨어날 시간을 지정할 수 있음. timeval로 지정하면 됨. timeout으로 빠져나오면 -1이 리턴되고, errno가 EAGAIN이 됨 (아래 SNDTIMEO도 마찬가지)
    • SO_SNDTIMEO
    • SO_RCVLOWAT
      • watermark인데 I/O를 발생할 최소단위의 크기를 지정한다. default로 1이다. (버전에 따라 지원정도가 다르다)
    • SO_SNDLOWAT
    • SO_TYPE
    • SO_ERROR
  • IPPROTO_IP
    • IP_TTL
    • IP_MULTICAST_TTL
    • IP_ADD_MEMBERSHIP
    • IP_DROP_MEMBERSHIP
    • IP_MULTICAST_LOOP
    • IP_MULTICAST_IF
  • IPPROTO_TCP
    • TCP_NODELAY
      • 1이면 Nagle Algorithm을 사용안함
    • TCP_MAXSEG
      • MSS크기 조절함(잘못 조절하면 ,... ㅡ.ㅡ;)

UDP Broadcasting

매우 간단하다. 소켓 옵션에 SO_BROADCAST를 주고 수신측 주소에 INADDR_BROADCAST를 주면된다. 특정 네트웍으로 broadcast하고 싶으면 해당 네트웍 주소를 주면된다. 소켓 옵션에 값을 줄때는 아래처럼 setsockopt를 사용한다.

int sockopt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &sockopt, sizeof(sockopt)) == -1) {
/* Handle error */
}

TCP Options

TCP의 성능 향상을 위해 사용되는 여러가지 방법들을 정리해보자.

TCP autotuning은 소켓 버퍼의 크기를 동적으로 조절하는 기능인데, 2.6.8커널 이후 버전에서 지원되며, 아래 값들을 통해 설정할 수 있다.

  • net.ipv4.tcp_moderate_rcvbuf : 수신쪽의 autotuning 설정

  • net.ipv4.tcprmem : 수신 버퍼의 최소/기본/최대값 (송신쪽은 ...wmem)

  • net.core.rmem_max : 지정할 수 있는 수신 버퍼 크기의 최대값 (송신쪽은 wmem)


값들은 sysctl 커멘드로 확인할 수 있다.

그외, TCP timestamp 는 RTT 측정 오차를 줄이는 방법이고. WSCALE이라고 16bit로 제한된 윈도우 사이즈를 늘려주는 옵션이다.

그리고.. 여러개의 패킷 유실에 대해 ACK를 개별적으로 보내주는 selective ack도 있다.

Monday, June 23, 2008

TCP vs UDP

ALSP 책에서는 segment와 fragmentation을 구별하여 정의하고 있다. 둘다 데이터를 분할하는 것이지만 후자는 재결합이 가능하도록 쪼개는 것이고, 전자는 재결합과는 상관없이 데이터를 쪼개는 행위 자체를 말한다. TCP에서는 IP레이어로 내려가기 전에 segment 단위로 데이터를 쪼개는데, 이때 사용되는 크기가 MSS(Maximum Segment Size)이다. TCP에서는 이렇게 미리 데이터가 분할되어 내려오기 때문에 나중에 MTU 사이즈에 의해 재 분할 되지 않으며, UDP의 경우 IP레이어에서 MTU사이즈로 분할 된다.

TCP는 그 이름이 Transmission Control Protocol인 것처럼 헤더에 제어 flag이 붙는데, 이들은 URG, ACK, PSG, RST, SYN, FIN 으로 구성되어 있다.

UDP의 경우는 별도의 control flag 이 없어서 단순하게 사용 가능하나, 전송하는 데이터그램이 버퍼크기보다 작아야 함을 주의해야 한다. 버퍼보다 큰 데이터그램이 들어오는 경우 Drop되기 때문이다. 이것은 netstat -s 로 UDP overflow가 있는지 확인해 보므로써 알 수 있다.

UDP

UDP는 별도의 연결 과정이 없다. 때문에 매번 보낼때 송수신 주소를 담아서 보내고 받는다. 그리고 broadcast, multicast 식으로 한번에 여러 곳으로 데이터를 송신할 수 있다. 일단 bind()로 바인딩 후 sendto(), recvfrom()으로 주고 받는다. 송수신이 완료되면 close(), shutdown()으로 소켓을 닫는다. 연결이 없어서 server, client 구분이 없다. 간단하다.

int sendto (int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

인자는, 뭐 대충,.. 소켓, 데이터, 데이터길이, 옵션, 송(수)신 주소, 주소 길이.. 식이다.

Friday, June 20, 2008

TCP Status

TCP연결시에는 알다시피 3way handshaking을 한다. close시에는 client, server 가 각각 FIN을 보낸 후 ACK를 받는다(총 4번의 트랜젝션).

TCP연결에는 여러개의 상태가 있는데, netstat으로 확인할 수 있는 상태들에 대해 간단히 정리해보면,

  • 서버에서 listen()이 호출되면 상태는 LISTEN으로 바뀐다.

  • 이 때 client에서 SYN을 보낸 후 SYN_SENT로 상태가 바뀌면

  • 서버에서 이를 받고 SYN_RCVD로 바뀌고 SYN ACK를 보내준다.

  • 그리고 client에서 SYN을 다시 받게 되면 비로소 ESTABLISHED로 바뀐다.

  • 이 후 send(), recv()군의 함수들로 계속 통신을 하다가

  • client에서 close()호출로 인해 FIN 을 보내고 FIN_WAIT1상태가 되면

  • 서버에서 이를 받고 ACK를 보낸 후 CLOSE_WAIT으로 상태를 바꾸고

  • client에서는 이를 받으면 FIN_WAIT2로 바뀌게 된다.

  • client에서 close할 준비가 끝났으므로, 이제는 서버에서 close()가 호출되면 FIN이 전송되고, 상태는 LAST_ACK이 된다.

  • client에서는 ACK를 보낸 후 TIME_WAIT상태가 되고, Maximum Segment Lifetime x 2시간 정도 기다리다가 소켓을 닫는다.

  • 서버에서는 이 ACK를 받고 CLOSED로 상태가 바뀐다.


close시 일반적으로 client에서 먼저 FIN을 보내고(active close), 서버는 이를 받으면 recv()에 EOF가 전달된다. 서버는 자신이 보낸 FIN에 대한 ACK를 받아야 close된다(passive close).

TIME_WAIT이 필요한 이유 client에서 ACK를 보냈는데, 서버가 못받으면, 서버는 LAST_ACK상태에서 일정시간이 지나도 ACK가 안오므로 FIN을 다시 보낸다. 이때 client가 이미 close 한 상태이거나 같은 포트번호로 다른 접속을 시도하고 있다면 처리할 수 없으니까, TIME_WAIT을 두어 다시 들어온 FIN에 대해 ACK를 주는 것이다. (client에서는 그래서 재접속시 다른 포트 번호를 받게 된다).

Friday, June 6, 2008

Threads, condition variables

Thread를 동기화하는 다른 방법 중의 하나로 Condition variable을 사용하는 것이 있다. condition variable을 잘 사용하면 race-free한 thread 코드를 만들 수 있다.

condition variable자체는 mutex 에 의해 보호된다. 때문에 condition 상태를 바꾸기 위해서는 먼저 mutex lock을 걸어줘야한다. condition variable(이하 cv) 초기화는 두가지 방법이 있는데, cv가 static이면 PTHREAD_COND_INITIALIZER를 넣어주면 되고 dynamic 이면(동적 할당되었으면) pthread_conf_init()을 쓰면된다. 사용이 끝나면 pthread_cond_destroy()를 호출해준다.

#include <pthread.h>
// 0 for OK
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);

cv를 체크하는 방법으로는 아래의 두 함수를 사용할 수 있다. 아래 wait함수를 호출할 때는 인자로 넣어주는 mutex가 lock된 상태여야 한다. 그러면 wait 함수 내부에서 cv를 기다리는 threads list에 함수를 호출한 thread를 넣고 mutex를 unlock한 후 wait상태에 들어가게 된다. wait하다가 condition 이 true가 되면 mutex를 lock한 상태로 리턴하게 된다. timedwait함수의 경우는 얼만큼 오래 기다릴지를 timespec으로 지정해줄 수 있다. timedwait의 경우 condition이 발생하지 않았는데 expire된 경우 lock을 한 상태에서 ETIMEOUT error를 반환한다.

#include <pthread.h>
// 0 for OK
int pthread_cond_wait(pthread_cont_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

wait 함수들은 condition이 발생했다는 것은 알려주지만 자신이 이를 처음으로 통보받았다고 보장하지는 않는다. (예를 들어 한개의 condition이 있고 4개의 thread 가 있다면 각각의 thread가 pthread_cond_wait()하다가 condition이 발생해서 pthread_cond_wait()을 빠져 나왔다면, condition이 발생한 것은 확실하지만, 제일 처음 이를 통보받은 thread가 condition과 관련된 부분을 변경했을 수도 있다는 것이다) 때문에 이를 위해 condition을 다시 체크해보아야 한다.

이번에는 반대로 condition을 변경해주는 부분을 생각해보자. 아래 함수들을 사용해서 두가지 방법으로 condition이 변경되었음을 알려줄 수 있는데, signal 은 기다리고 있는 thread중 하나만 wakeup시켜주고 broadcast는 모든 thread를 깨워준다.

#include <pthread.h>
// 0 for OK
int pthread_cond_sigal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

아래 APUE 에서의 간단한 예제를 살펴보자.

#include <pthread.h>

// queue의 노드
struct msg {
struct msg *next;
/* ... */
}

struct msg *workq;
// condition variable
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
// mutex to protect c.v.
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg(void)
{
struct msg *mp;
while (1) {
pthread_mutex_lock(&qlock);
// 이미 lock했어도 아래 ...wait 에서
// condition variable을 등록 후 내부
// 적으로 unlock하기 때문에 block상
// 태가 된다.
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
// wait 에서 condition 이 바뀐 것을
// 통보 받고 lock된 상태로 빠져 나왔음
// 아래 enqueue_msg 에서 broadcast로
// 통보 했다면 여기서 queue가 비었는지
// 다시체크해야하나, 그렇게 할 경우
// thundering herd가 발생할 소지가 있음

// 원래 queue는 tail에서 값을 뽑아야 하는데,
// 여기서는 head에 넣었다가 head 에서 뽑는다
// (FIFO가 아니 LIFO,..stack이다..)
// 이유는... 글쎄 ㅡ.ㅡ;..
mp = workq;
workq = mp->next;
pthread_mutex_unlock(&qlock);
// processing msg here
}
}

void enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}

Thread, manager worker model


Worker manager model 을 구현할 때 thread를 사용할 수 있다. 외부로 부터 요청된 job들을 관리하기 위해 manager는 일반적으로 queue를 사용한다. job 이 들어오면 어떤 thread가 처리해야할 지 판단한 후에 thread id와 함께 해당 job을 queue에 넣는다. 그럼 각각의 thread들이 queue를 보면서 자신에서 어떤 job이 assign되었는지 확인하고, 꺼내어 작업을 처리할 수 있다.
따라서 worker 나 manager 가 접근할 때 queue 가 lock 되어있어야 하는데, 이럴 때 mutex보다는 읽을 때는 shared mode 로 lock 할 수 있는 rwlock 을 사용한다.

Thursday, June 5, 2008

Thread, reader-writer locks

mutex는 잠금/해제 두가지 상태밖에 없다. 때문에 한 thread가 읽는 작업을 수행하는 동안 다른 thread들은 읽을 수 없다. reader-writer lock은 잠금의 경우가 read lock/write lock 두 가지 상태가 있기 때문에 여러 thread가 동시에 읽을 수 있다. 사용되는 함수들은 아래와 같다.

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 0 for OK

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 0 for OK

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrwlock(pthread_rwlock_t *rwlock);
// 0 for OK

맨 위의 init/destroy 함수는 mutex를 만들때 사용하는 함수이고, rdlock, wrlock은 각각 읽기/쓰기를 lock하는 함수이다(lock될 때 까지 block되어 기다린다). tryrdlock, tryrwlock들은 non-block으로 동작하는 함수들이다.

rdlock으로 lock되어 있는 경우, shared mode로 lock된 것이고, wrlock으로 lock한 경우에는 exclusive mode로 lock한 것이다.

write lock이 된 상황에서 다른 thread들이 rwlock/wrlock 을 하려는 경우 lock이 풀릴 때까지 모두 block되는 반면, read lock이 걸린 경우에는 read lock을 거는 thread들은 바로 접근이 가능하게 되고, write lock을 시도하는 경우에는 걸려있는 모든 read lock들이 해제될 때까지 block 된다.

비동기 lock 함수들을 사용하는 경우 lock할 수 없는 경우 리턴 값으로 EBUSY에 해당하는 값을 반환한다.

Socket, getsockname

이미 binding 된 socket으로 부터 주소정보를 받아올 때 getsockname을 사용한다. 아래와 같이 사용하면 된다.

getsockname(sfd, (struct sockaddr *)&saddr, &len_saddr);

Wednesday, June 4, 2008

Socket, thundering herd problem

network 프로그래밍시 주의해야할 문제 중에 하나로 thundering herd problem이 있다. 천둥이 치면 소떼가 뛴다...는 상황을 비교해서 쓰는 말인데, 여러 thread/process 간에 공유된 자원이 있을 때 자원이 사용가능하게 되지마자 block되어 있던 모든 프로세스들이 깨어나지만 한 프로세스만 자원을 점유하고 나머지는 다시 block되어야 하므로 CPU time을 낭비하게 된다. Mutex 같은 lock을 통해 이런 문제를 해결할 수 있다고 하는데,.. 그런가 ㅡ.ㅡ;,..

Monday, June 2, 2008

Socket, code example

간단히 ... 잊지 않을 정도의 코드 예제를 적어본다.
server side

#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int sfd, rsfd;
struct sockaddr saddr; // listener
struct sockaddr rsaddr;

sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

saddr.sin_family = AF_INET;
saddr.sin_port = 10000;
saddr.sin_addr.s_addr = inet_addr("100.100.100.100");
// or inet_aton

bind(sfd, (struct sockaddr*)&saddr, sizeof(saddr));

listen(sfd, BACKLOG);

rsfd = accept(sfd, (struct sockaddr *)&saddr, &len_rsaddr);

recv(rsfd, rbuf, sizeof(rbuf), 0);

send(rsfd, sbuf, sizeof(sbuf), 0);

shutdown(rsfd, SHUT_RDWR); // or close

client side

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int sfd;
struct sockaddr saddr;

sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

saddr.sin_family = AF_INET;
saddr.sin_port = 10000;
inet_aton("100.100.100.100", &saddr.sin_addr);
// or inet_addr

connect(sfd, (struct sockaddr*)saddr, sizeof(saddr));

send(sfd, sbuf, sizeof(sbuf), 0);

recv(sfd, rbuf, sizeof(rbuf), 0);

close(sfd); // or shutdown

Thread, mutex - deadlock/race condition avoidance

여러개의 thread가 같은 메모리에 접근할 때 발생할 수 있는 문제가 deadlock, race condition이다. deadlock을 만드는 가장 간단한 방법은 같은 mutex에 두번 lock을 거는 것이다. 혹은 두 thread A,B가 있고 mutex X,Y가 있을 때 A가 X를 lock하고 Y자원을 사용하고자 기다릴때 B가 Y를 lock하고 X자원을 사용하고자하는 경우 이런 상황이 발생한다. thread에서 이런 상황을 피하는 방법으로는 아래의 두가지가 있다.

  • 여러 mutex를 사용하는 경우 lock하는 순서를 항상 동일하게 한다. (위의 예라면 항상 X를 lock한 후 Y를 lock하는 식으로 정해서 deadlock을 피할 수 있다)

  • 자신이 사용하고자 하는 자원이 다른 thread에 의해 사용중인 경우 자신이 점유하고 있는 자원을 해제한 후 기다린다.


linux에서 사용가능한 mutex관련 함수로는 아래 것들이 있다.

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

mutex를 사용하기 위해서는 ..._init(), ..._destroy() 함수들을 사용해야한다. ..._init()함수 사용시 mutex의 종류를 정해줄 수 있는데, 아래와 같은 매크로로 지정할 수 있다.

  • PTHREAD_MUTEX_NORMAL : linux에서는 "timed" mutex이다.

  • PTHREAD_MUTEX_RECURSIVE : 여러번 잠글 수 있는 mutex

  • PTHREAD_MUTEX_ERRORCHECK : 에러 체크해주는 mutex

  • PTHREAD_MUTEX_MUTEX_DEFAULT : linux에서는 첫번째의 ..._NORMAL과 같다.


위에서 설명한 "timed mutex"는 기본 mutex인데, 중복으로 lock을 걸면 deadlock에 걸리고, 다른 thread가 lock한 상태에서 내가 풀려고 하면 undefined 상태가 된다. errorcheck 타입은 데드락의 경우 에러를 리턴해준다. 그 외에 adaptive mutex가 있는데, 표준은 아니고, 플랫폼에 따라 최적의 매커니즘으로 동작하게 된다. linux에서 SMP가 지원되는 경우 spinlock을 사용하게 되는데, 이때 adaptive mutex를 사용하게 되면 짧은 시간동안의 lock, unlock에 대해 최적의 성능을 지원한다. 대신 비 표준이라 PTHREAD_MUTEX_ADAPTIVE_NP를 통해 초기화 한다.

이런 mutex 타입을 mutex attribute 함수들을 통해 설정하여 mutex init시 사용하면 된다.

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutex_gettype(pthread_mutexattr_t *restrict attr);
int pthread_mutex_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);