Friday, May 23, 2008

Pointer's pointer - 포인터의 포인터

함수 인자에 포인터의 포인터를 사용하는 경우가 있으나, 가끔씩이라 햇갈릴 때가 있다. 어떤 경우에 사용하게 되는지와 그 이유를 정리해보자.



그림처럼 int a와 int *pa 가 있다고 치자. pa = a로 pa가 a를 가리키는 상황이면 pa는 a의 주소인 0x1001을 가지고 있다. 그래서 pa = 0x1001, *pa = 3 이다. 정수 포인터(int *)를 인자로 받는 함수 fa에서는 a값에 대해서만 참조가 가능하다. 그런데, 포인터가 가리키는 값을 바꾸고 싶은 경우가 있다. (스택이나 리스트 같은 자료구조를 구현하다 보면 필요하게 된다. 스택 포인터를 인자로 받는 push(node *stack, void* data) 같은 경우를 생각해보자. 새로운 공간을 할당하여 data를 넣은 후 이를 stack의 top node로 하고 node->next = stack로 바꿔줘야한다. 그러나 node *만을 받아서는 이를 변경하는 것이 불가능하다.) 그래서 사용하는 것이 포인터의 포인터이다. fb와 같이 포인터의 포인터를 인자로 받는 함수를 생각해보자. 함수 fb (int **ppa) 에 대해서 fb(&pa) 와 같이 인자를 주었다면, 함수에는 pa의 주소인 0x2001이 들어간다. 그래서 함수 fb 안에서는 ppa = 0x2001, *ppa = 0x1001, **ppa = 3 으로 포인터의 주소, 포인터가 가리키는 값의 주소, 포인터가 가리키는 값 세가지를 모두 접근할 수 있으며, 포인터가 다른 데이터를 가리키도록 바꿀 수도 있게 된다(스택에서는 그래서 push(node **stack, void* data) 와 같은 식이 되어야 한다). 이를 위해 포인터의 포인터가 사용된다.

Socket, TCP flow of both sides

TCP 소켓 통신은 서버와 클라이언트에 따라 아래와 같은 순서로 함수 호출이 일어난다.

  • Server

    socket() - bind() - listen() - accept() - recv(), send() - close()(passive)

  • Client

  • socket() - bin() - connect() - recv(),send() - close()(active)

웹상에 이쁜그림이 없어 간단히 그려보았다. (client에서는 connect()가 bind()를 포함 하므로 생략해도 된다)

Signals, sending signal with payload

프로세스에 signal을 보낼 때 payload 를 줄 수 있다. kill(), raise()가 아닌 sigqueue()를 사용하는데, 아래와 같다.

#include <signal.h>

union sigval {
int sigval_int;
void *sigval_ptr;
}

int sigqueue (pid_t pid, int signo, const union sigval value);

그래서 아래와 같이 간단히 payload를 줄 수 있다.

sigval value;
int ret;

value.sigval_int = 404;

ret = sigqueue(4500, SIGUSR1, value);
if (ret)
perror("sigqueue");

Thursday, May 22, 2008

Socket, byte order

사용하는 시스템의 architecture가 big-endian인지 little-endian인지를 알아야 보낼 데이터를 socket에서 사용하는 endian에 맞게 고쳐 보낼 수 있다. system의 endian을 알기 위한 테스트는 아래처럼 할 수 있다.

union btye_long {
long l;
unsigned char c[4];
}

int main()
{
union byte_long bl;
bl.l = 1200000L;
printf ("%02x-%02x-%02x-%02x\n", bl.c[0], bl.c[1], bl.c[2], bl.c[3]);
bl.l = htonl(bl.l);
printf ("%02x-%02x-%02x-%02x\n", bl.c[0], bl.c[1], bl.c[2], bl.c[3]);
return 0;
}
// from ALSP

근데, 이건 좀 복잡하고, 사실 if (120000L == htonl(120000L)) 만 해도 알 수 있다 ㅡ.ㅡ; 인텔 계열은 little endian 인데 반해 network packet에서는 bigendian을 사용한다. 그래서 항상 byte order를 변경해주어여 하는데 이때 사용하는 함수군이 아래와 같다.

  • htons host endian to network endian converting of 2bytes short

  • htonl host endian to network endian converting of 4bytes long

  • ntohs network endian to host endian converting of 2bytes short

  • ntohl network endian to host endian converting of 2bytes long


double, float 등의 변환은 ... 일단 생략 --;

Signals, advanced management

signal()을 통한 signal 처리는 기본적인 것이다. 조금 진보된 형태의 signal 처리로 sigaction()이 있다.

#include <signal.h>
int sigaction (int signo, const struct sigaction *act, struct sigaction *oldact);

sigaction()은 핸들러가 돌아가는 동안에 특정 signal을 block할 수도 있고, 핸들러가 프로세스의 상태에 대한 여러 정보들을 제공받을 수도 있다. 위에서는 signo에 해당하는 signal에 대한 핸들을 act로 바꿔주며, 이전의 핸들(behavior)을 oldact에 담아준다. 핸들을 등록할 떄 사용하는 sigaction struct에 대해 간단히 알아보자.

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); /* OBSOLETE */
}

이 구조체에서는 두가지 형태의 핸들러를 등록할 수 있다. sa_handler는 signal()에서 등록하던 핸들러와 같은 형태이고, sa_sigaction은 sa_flags에 SA_SIGINFO 가 켜져있는 경우 사용하는 핸들러인데 아래와 같은 형식을 취한다. 이 flag에 따라 둘 중 하나의 핸들러를 사용하기 때문에 어떤 시스템의 경우에는 이 부분을 union으로 정의하기도 하므로 두개의 핸들러를 모두 등록하는 일은 되도록 피하자. sa_flags에 SA_NODEFER가 켜져있지 않는 한 처리하고 있는 signal과 동일한 signal은 block되며 다른 signal의 block은 sa_mask를 통해 추가할 수 있다(물론 SIGKILL, SIGSTOP은 block안된다).

void handler(int signo, siginfo_t *si, void *ucontext);

인자로 signal 번호뿐 아니라 siginfo_t도 받고, void *인 ucontext도 받는다. 다시 sigaction 구조체로 돌아가서 sa_flag 얘기를 더하면, 아래와 같은 flag들을 사용할 수 있다.

  • SA_NOCLDSTOP child가 stop, resume되어도 noti안한다.

  • SA_NOCLDWAIT child 에 대해 wait() 안하겠다...

  • SA_ONSTACK signal이 왔을 때 alternative signal stack을 쓰겠다는 것인데... 잘모르겠다.

  • SA_RESETHAND 'one-shot' 모드이다. 현재 핸들러를 이번 한번만 쓰고 다음에는 default 핸들러를 쓰겠다는...

  • SA_RESTART signal에 의해 인터럽된 system call을 BSD style로 restart하겠다는 건데..ㅡ.ㅡ;.


SA_SIGINFO 일때 사용하는 핸들러에 전달해 주는 siginfo_t 구조체에 어떤 정보들이 있는지 살펴보자

typedef struct siginfo_t {
int si_signo;
int si_errno;
int si_code;
pid_t si_pid;
uid_t si_uid; /* process real UID */
int si_status;
clock_t si_utime;
clock_t si_stime;
sigval_t si_value; /* payload */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
void *si_addr; /* dault시 memory location */
int si_band;
int si_fd;
}



  • si_signo받은 signal number

  • si_errno이 signal과 관련된 error code

  • si_pid종료된 프로세스의 pid(SIGCHLD signal인 경우 사용)

  • si_uid(SIGCHLD signal인 경우 사용)

  • si_statusexit status (SIGCHLD signal인 경우 사용)

  • si_utime종료된 프로세스가 소비한 user time(SIGCHLD signal인 경우 사용)

  • si_stime종료된 프로세스가 소비한 system time(SIGCHLD signal인 경우 사용)

  • si_valueunion of si_int, si_ptr

  • si_intpayload type이 int인 상태에서 sigqueue()에 보내진 signals

  • si_ptrpayload type이 void *인 상태에서 sigqueue()에 보내진 signals

  • si_addrSIGBUS,SIGFPE,SIGILL,SIGESV 등인 경우 fault가 일어난 address를 가지고 있음

  • si_band OOB 인 경우 fd의 oob, priority info를 가지고 있음

  • si_fd SIGPOLL 일때 작업이 완료된 파일의 fd를 가지고 있음



siginfo_t의 다른 원소인 si_code는 signal을 누가 발생 시켰는지, 왜 발생했는지에 대한 정보를 알려주는데, 몇개만 살펴보면 아래와 같다.

  • SI_ASYNCIOasynchronous I/O 끝나서 발생

  • SI_KERNEL커널이 보냈음

  • SI_TIMERPOSIX timer expired

  • SI_USER사용자가 kill(), raise()로 보냈음

  • SIGCHLD 인경우에는 아래의 si_code 들이 넘어온다.

  • CLD_CONTINUED child stopped but resumed.

  • CLD_DUMPED child terminated abnormally.

  • CLD_EXITED child terminated via exit().

  • CLD_KILLED child was killed.

  • CLD_STOPPED child stopped.

  • CLD_TRAPPED child hit a trap.

  • POLL_ERR I/O error

  • POLL_HUP device hung up or disconnect

  • POLL_IN available to read

  • POLL_MSG message is available

  • POLL_OUT available to write

  • POLL_PRI available high-priority data to read


그외에 SIGFPE, SIGBUS, SIGILL 등의 경우는 넘어가자.

Wednesday, May 21, 2008

Socket, create

기왕 공부하는거 socket도 복습해보자.

소켓은 도메인에 따라 unix socket, network socket 으로 구분하는데 앞의 것은 한 host 내에서 사용할 때이고 뒤의 것은 여러 호스트에 걸친경우 이다.

소켓은 타입에 따라 datagram 이냐, stream이냐의 차이를 가지는데 데이터를 일정한 단위로 나누어 보내냐 혹 stream 형태로 연결을 유지하면서 보내냐의 차이다(거의 UDP, TCP의 차이).

소켓은 아래처럼 간단하게 만들어진다.

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// domain AF_UNIX (unix socket),
// AF_INET(network socket) (AF = PF)
// type SOCK_STREAM
// SOCK_DGRAM
// SOCK_RAW
// protocol IPPROTO_IP (0) (for both)
// IPPROTO_TCP (for SOCK_STREAM)
// IPPROTO_UDP (for SOCK_DGRAM)
// IPPROTO_ICMP
// ex.
if ((sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP)) == -1)
fprintf(stderr, "error socket\n");
if ((sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP)) == -1)
fprintf(stderr, "error socket\n");


SOCK_DGRAM(UDP)이나 SOCK_STREAM(TCP)이나 IP 아래에서 동작하므로, 위처럼 IPPRORO_IP를 가지고 생성할 수 있다.

Signals, signal block with masking

프로세스는 여러개의 signal을 block 시킬 수 있는데,이 때 sigprocmask()를 사용한다.

#include <signal.h>
int sigprocmask (int how, const sigset_t *set,
sigset_t *oldset);

how에 SIG_BLOCK 를 주면 sigset에 해당하는 signal들이 block되고, SIG_UNBLOCK을 주면 해제된다. SIG_SETMASK는 ... 잘 모르겠다 ㅡㅡ; set이 NULL 인 경우에는 현재 block 되어 있는 signal들이 oldset에 찍혀 나온다.

이런식으로 block한 signal들은 unblock하면 프로세스에 전달되는데, pending된 signal들은 sigpending(sigset_t *set)을 통해 확인할 수 있다. 일반적으로 signal을 고려한 critical section의 처리는 아래와 같은 순서로 진행된다.

  • sigprocmask()로 특정 signal들을 block한다.

  • critical section 을 처리한다.

  • sigsuspend()로 block한 signal 들이 처리될 때 까지 기다린다.


이러한 식으로 signal set을 통해 간단한 코드를 짜보면,

#include <signal.h>
#include <stdio.h>

void sig_handler(int signo)
{
fprintf(stderr, "signal caught : %s\n", sys_siglist[signo]);
return;
}

int main()
{
sigset_t sigset;
sigset_t sigset_old;
sigset_t sigset_test;

if (signal(SIGINT, sig_handler) == SIG_ERR) {
fprintf(stderr, "SIGINT handle can't be registered\n");
}
if (signal(SIGQUIT, sig_handler) == SIG_ERR) {
fprintf(stderr, "SIGINT handle can't be registered\n");
}

sigemptyset(&sigset);
sigaddset(&sigset, SIGQUIT);
sigprocmask(SIG_BLOCK, &sigset, &sigset_old);
pause();
sigpending(&sigset_test);
if (sigismember(&sigset_test, SIGQUIT))
printf("SIGQUIT is pending\n");
sigsuspend(&sigset_old);
//sigprocmask(SIG_UNBLOCK, &sigset, NULL);
return;
}


SIGINT, SIGQUIT 핸들러를 등록하고, sigset에 SIGQUIT을 넣고 sigprocmask로 블럭한다음 pause()로 signal을 기다린다(SIGQUIT이 block되어 있기 때문에 Ctrl+\ 을 눌러도 반응이 없다). Ctrl-c를 누르면 block되지 않은 signal 이므로 pause()를 빠져나오고 handler가 호출된다. 그리고 sigpending()에서 현재 pending 된 signal을 확인한다. Ctrl+c전에 Ctrl+\를 눌렀다면 SIGQUIT이 pending되었다고 나오게 된다. 그리고 마지막 두줄이 중요한데, sigsuspend(&sigset_old) 를 호출하면 프로세스가 sigset_old에 있는 SIGQUIT signal이 일어나서 처리 될때까지 대기한다(때문에 SIGQUIT이 들어오지 않으면 계속 기다리고 있는다). sigsuspend 대신에 sigprocmask 로 UNBLOCK하면 그 순간 pending되어 있는 signal 이 프로세스에 전달되는데, pending 되어 있는 signal이 없으면 그냥 넘어간다. 즉 sigsuspend()를 쓰면 signal이 일어날때까지 기다리고, sigprocmask UNBLOCK을 쓰면 signal이 있을 때 handler가 호출되고 없으면 그냥 넘어가는 차이가 있다.