Thursday, May 29, 2008

Thread, basic II

thread에 사용되는 함수들은 아래와 같다.

  • pthread_create()
    thread를 만든다.

  • pthread_exit()
    종료할때 return 대신 쓰면 cleanup함수들이 실행된다.

  • pthread_join()
    다른 thread가 종료될 때까지 현재 코드를 block한다. 종료될 때 그 thread의 리턴값을 받을 수 있다.

  • pthread_cancel()
    다른 thread에게 종료 신호를 보낸다. 종료하는 건 신호를 받은 thread 맘이다. 종료하게 되면 pthread_exit()가 호출된 것처럼 동작한다.

  • pthread_cleanup_push()
    종료시 실행할 함수를 등록한다 like atexit(). 종료시 stack처럼 최근 등록된 순서대로 pop되면서 실행된다.

  • pthread_cleanup_pop()
    위의 ..._push()함수로 등록한 것을 최근 순서로 해제(pop)한다. 단 인자가 0이 아니면 실행하면서 pop한다.

Socket, connect()

client에서 서버로 접속할 때는 connect()를 사용한다.

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addr_len);

보다시피 사용방법은 아주 간단하다. 먼저 socket을 만들고, 만들 소켓을 접속한 주소가 담긴 sockaddr와 함께 인자로 주면 된다. 입력한 주소가 invalid한 경우에 0을 리턴하기 때문에 아래와 같이 코드를 만들게 된다.

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

int sock_fd;
int saddr;

sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

saddr.sin_family = AF_INET;
saddr.sin_port = 80; // port

if (inet_aton("100.100.100.100"), &(saddr.sin_addr) == 0)
fprintf(stderr, "error : inet_aton\n");
if (connect(sfd, (struct sockaddr *)&saddr, sizeof(saddr)) == -1)
perror("connect");

Tuesday, May 27, 2008

Socket, bind(), listen() and accept()

socket()을 통해 소켓을 생성했다면 다음은 bind()이다. bind()는 아래와 같다.

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

sockfd는 앞서 만든 socket이고, sockaddr은 IP/Port 등의 주소 정보를 가진 구조체다. addrlen은 이 구조체의 크기이다. Unix socket을 사용하는 경우 sockaddr_un을 사용하지만, Network socket을 사용하는 경우 sockaddr_in을 사용한다. 두 구조체 모두 bind에 넣을 때는 (sockaddr *)로 캐스팅해서 넣어준다(bind내부에서 sockaddr로 캐스팅된 구조체의 앞부분을 보고 network socket인지 unix socket인지 알수 있다). 구조체를 잠깐 살펴 보자.

struct sockaddr_in {
unsigned short sin_family; // AF_INET
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8]; // not used
}
struct sockaddr_un {
short sun_family;
char sun_path[108];
}
struct in_addr {
unsigned long s_addr;
}


특별한 건 없고, AF_INET/AF_UNIX, 주소, 포트 번호 등이 들어가 있다. 주소는 in_addr 구조체로 들어가 있는데, unsigned long 타입으로 되어 있다. IP주소가 x.x.x.x 식으로 되어 있고 x < 256 이므로(8비트), 32bit 만 있으면 충분하다. 그래서 in_addr의 s_addr이 unsinged long (32bit) 이다. 여기에 INADDR_ANY (0)을 넣으면 서버에 지정된 모든 IP에 해당된다. 여기에 주소를 넣을 때는 inet_addr(), inet_aton()을 사용할 수 있는데, 전자의 경우에는 unix 표준이지만, 리턴되는 -1값이 INADDR_NONE(255.255.255.255) 인 경우와 에러인 경우를 모두 포함하므로 구별하기가 모호하다. inet_aton은 구분이 가능하지만 표준에 들어있지 않아.. 거시기 하다ㅡㅡ; 암튼 대충 이렇고, 실제로 주소를 넣는 코드는 아래와 같다.

struct sockaddr_in saddr_svr;
saddr_svr.sin_family = AF_INET;
saddr_svr.sin_port = htons(12000); //port number
// 방법1
saddr_svr.sin_addr.s_addr =
inet_addr ("192.168.100.20");
// 방법2
if (!inet_aton("192.168.100.20", &(saddr_svr.sin_addr.s_addr)))
{
fprintf (stderr, "inet_aton");
}

bind는 성공시 0을, 실패시 -1을 리턴하고 errno를 설정해준다. 위처럼 설정한 sockaddr_in을 아래처럼 binding하면된다.

if (bind(sock_fd, (sockaddr *)saddr_svr, sizeof(saddr_svr)) == -1)
perror("bind");

bind된 socket으로 실제 TCP 요청을 받기 위해(TCP 접속을 위해) listen()을 사용한다. 인자로 backlog 값이 사용되는데, 커널에서 큐로 구현되어 있으며 default로 1024 로 되어 1024개 까지 TCP 접속을 허용할 수 있게된다.

int listen (int s, int backlog);

생성된 소켓으로 외부 요청을 받을 수 있도록 주소를 binding하고 listen 하기 시작했다면, 요청을 기다리다가 들어온 요청을 accept() 하여 받을 수 있다. accept 함수는 bind 소켓과 client의 주소정보를 담을 sockaddr 구조체를 인자로 주면 요청이 들어올 때까지 코드를 block 한다.

int client_sockfd;
struct sockaddr_in client_saddr;
socklen_t client_socklen;

client_socklen = sizeof(client_saddr);
client_sockfd = accept(sock_fd, (struct sockaddr *)&client_saddr, &client_socklen);

printf("client : %s:%d\n", inet_ntoa(client_saddr.sin_addr), ntohs(client_saddr.sin_port));

close(...);

위 코드의 실행시 client의 요청에 대해 아래와 같이 출력된다.

./server 127.0.0.1 21650
Client : fd(4) 127.0.0.1:35130

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가 호출되고 없으면 그냥 넘어가는 차이가 있다.

Monday, May 19, 2008

Thread, Basic

LSP에서는 thread를 다루고 있지 않다. 그래서 APUE의 내용을 통해 복습해 보려고 한다. 알다시피 thread는 한 프로세스 내에서 여러개로 되어 동작하기 때문에 프로세스 내의 메모리나 fd를 공유한다. 그렇기 때문에 이러한 프로세스 내 공유 자원들에 접근할 때는 항상 consistency를 생각해야 한다. thread를 사용하는 일반적인 장점들은 아래와 같다.

  • asyncronous event를 다루는 루틴을 event type에 따라 별개의 thread를 만들어 처리하면 코드가 간단해진다.

  • 여러 프로세스들을 사용하면서 fd, memory를 공유하려면 복잡한 커널의 system call을 사용해야하는 반면에 thread에서는 간단하게 처리할 수 있다.

  • 한 thread에서 직렬로 진행되던 루틴들이 병렬화 되어 성능 향상을 가져올 수 있다.

  • Single processor에서도 성능향상이 있을 수 있다(한 thread가 block일때 다른 thread는 돌 수 있으므로)

  • multiple thread를 사용하므로써 사용자 응답시간이 좋아진다(user input/output을 thread로 돌리면 되므로) .

  • Multi-processor 환경에서 성능 향상을 가져다 준다.


한개의 thread는 여러 정보들을 가지고 있는데 아래와 같다.

  • thread ID, 레지스터 값들, 스택, 스케쥴 priority, policy, signal mask and errno


소위 pthread라 불리는 것들은 POSIX.1-2001의 optional feature이다. 이게(optional한 부분이) 지원되는지 안되는지는 _POSIX_THREADS 를 #ifdef로 테스트 해보면 된다(아님, sysconf에서 _SC_THREADS로 호출해보거나..)

thread identification pid_t 처럼 thread에서는 pthread_t 를 쓴다. 그런데, 구현에 따라 pthread_t를 구조체로 만드는 경우도 있기 때문에 tid1 == tid2 식으로 비교할 수는 없고 아래 함수를 사용한다. 자신의 tid를 보기 위해서는 pthread_self()를 사용한다.

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
pthread_t pthread_self(void);

일단 thread 만드는 함수를 알아야 할텐데, 아래와 같다.

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void), void *restrict arg);

좀 어려워 보이지만 인자부터 체크해보자, 첫번째는 tid가 반환 될 것이고 두번째는 attributes를 넘겨줄 때 쓴다. 새로 생성된 thread는 start_rtn 루틴 부터 시행을 시작할 것이고, 이 함수는 arg를 인자로 받는다. thread는 signal mask를 상속받지만, pending 되어 있던 signal들은 clear된다. 조심할 점은 thread가 만들어지면 어떤 것이 먼저 실행될지 알 수 없다는 것이다. 대충 알았으니 한번 만들어보자.

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

pthread_t ntid;

void * thr_func(void *arg)
{
printf("new thread id(%u)pid(%u)\n",
(unsigned int)pthread_self(), (unsigned int)getpid());

return (void *)0;
}

int main()
{
int err;

err = pthread_create(&ntid, NULL, thr_func, NULL);
if (err != 0)
fprintf(stderr, "pthread_create error\n");

printf("main thread id(%u)pid(%u)\n",
(unsigned int)pthread_self(), (unsigned int)getpid());
sleep(1);
return 0;
}

linux에서는 pid가 다른 경우도 있고, tid는 4자리로 나타나는 것으로 되어 있는데, 실제 돌려보니 pid가 같고, tid도 연관성이 없는 것처럼 보인다(FreeBSD처럼 나온다) 왜일까. pthread_self 대신 gettid()를 사용하면 pid와 비슷하게 생긴 tid를 얻을 수 있다.

Friday, May 16, 2008

Signals, signal set

편리하게, 여러 signal을 묶어서 처리할 수 있다. 묶어진 signal set을 통해 특정 signal들을 block하거나, block을 통해 pending된 signal을 처리하고 혹은 특정 signal들을 기다리는 것도 가능하다. 조작에 쓰이는 기본적인 함수들은 아래와 같다.


#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
int sigisemptyset(sigset_t *set);
int sigorset(sigset_t *dest, sigset_t *left, sigset_t *right);
int sigandset(sigset_t *dest, sigset_t *left, sigset_t *right);

sigset_t 는 signal들을 bit에 하나씩 맵핑시킨 것인데, sigemptyset은 이를 0으로 AND 시키고, sigfullset 은 0xF...F 로 OR 시킨다고 생각하면된다. sigandset 은 left, right를 and해서 dest에 넘겨주고, sigorset은 OR한다. 나머지 함수들은 직관적이므로 생략.

Signals, reentrancy

signal을 처리하고 있는데 다른 signal이 들어와서 중간에 다른 루틴을 수행한다면? 그 과정이 계속 반복된다면? 혼란스러운 일이다. 그래서 signal을 처리할 때는 이를 약간 고려해줘야한다. global 데이터는 손데지 않고, shared data도 안건드리는 것은, 좋은 습관이다. handler에서 malloc()을 호춣거나 strsignal()과 같이 static buffer를 사용하는 경우 중간에 들어오는 signal에 대해 어떻게 처리될지 알아두는 것은 좋은 일이다. reentrant function 이라함은 같은 프로세스내의 다른 thread에 의해 호출되어도 '안전'한 경우를 말한다. 이를 위해서는 아래 내용들이 지켜져야한다.

  • static data를 조작하면 안되고,

  • stack에 있는 데이터나 caller가 준 데이터만 조작한다

  • 다른 nonreentrant function을 호출하지 않는다,


재진입가능한 함수들에 대해서는 ... 종류가 많은데 필요하면 그때 책을 참조하는 것으로 하자. 일반적으로 abort, bind, connect, fork, fsync, kill, read, send, signal, time, wait, open, pipe, select, poll, recv, sock 등은 reentrant하지만, malloc 등은 그렇지 않다.

Wednesday, May 14, 2008

Signals, basic manipulation

아주 기초, 기본적인 signal 처리함수는 아래와 같다.

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signo, sighandler_t handler);

signo에 해당하는 signal이 발생할 때 handler 함수를 호출하겠다는 얘기다.

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

void handle_sigtstp(int signal)
{
printf("ctrl-z pressed. "
"You shouldn't use printf!\n");
return;
}

int main()
{
signal(SIGTSTP, handle_sigtstp);

sleep(10);
}

위 예제 처럼 간단하게 등록, 사용할 수 있다. printf 같은 함수를 호출하면 안되는 이유는 이 함수가 reentrancy를 보장하기 않기 때문인데, 일단 나중에 얘기하자.

사용자 handler 대신에 default로 동작하기를 원하는경우 handler 대신 'SIG_DFL'을 주면 되고, signal을 무시하고자 하는 경우에는 SIG_IGN을 주면된다. signal()은 등록된 handler의 포인터나 SIG_DFL, SIG_IGN 을 주는데, 실패한 경우 SIG_ERR을 준다(errno를 설정하지는 않는다)

signal을 테스트할 때 편한 함수로 pause()가 있다(signal을 받을 때 까지 기다린다). 이러한 부분을 고려하면 코드는 아래와 같아진다.

if(signal(SIGINT, handler) == SIG_ERR) {
fprintf(stderr, "Can't handle SIGINT\n");
}

프로세스가 실행(exec)되면 Signal handle은 default로 잡힌다. 부모 프로세스가 특정 signal을 무시하고 있었다면, exec된 프로세스도 무시하게 된다. 그리고 부모 프로세스가 잡아서 처리하던 signal은 default action으로 바뀐다(부모 프로세스의 주소공간을 공유하고 있지 않아서 handler도 공유/상속되지 않기 때문이다).

쉘에서 특정 job을 bg로 돌리는 경우 bg로 돌아가는 job은 SIGINT, SIGQUIT 등을 무시해야 한다. 그래서 쉘에서 bg job들을 실행하기 전에 아래처럼 SIGINT, SIGQUIT을 무시하는 코드를 수행한다(그래야 bg job들도 무시하게 되니까)

if(signal(SIGINT, SIG_IGN) != SIG_IGN) {
if(signal(SIGINT, sight_handler) == SIG_ERR) {
fprintf(stderr, "Can't handle SIGINT\n");
}
}
/* SIGQUIT도 마찬가지 */

그런데 fork()의 경우에는 좀 다르다. 부모의 주소공간을 공유하기 때문에 signal handler 까지 모두 받아서 똑같이 signal을 처리한다.

특정 signal이 발생했을 때 SIGINT인지 SIGKILL인지 숫자로 되어있어 햇갈릴 수 있다. 그래서 sys_siglist[] 를 통해서 각각 signal을 string으로 찍어볼 수 있다. linux에서 char *strsignal(int signo)를 제공하지만 표준은 아니다(thread-safe하지도 않다). sys_siglist[]를 추천한다.

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

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

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

signal을 보내는 방법은 아래처럼 매우 간단하다. 혹은 kill로 바로 보내도 된다(like, kill -HUP [pid]).

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int signo);

kill()은 세가지 값을 반환하는데, invalid signal인 경우 EINVAL, 권한이 없는 경우 EPERM, pid가 없거나 좀비인 경우 ESRCH이다.

다른 user나 그룹등...에 signal을 보내고자 하는 경우에는 프로세스가 CAP_KILL capability가 있어야 한다. 내가 다른 프로세스에 signal을 보낼 권한이 있는지 체크하는 좋은 방법으로는 signal로 '0'을 보내보는 것이다(실제 signal을 보내지는 않으므로 err만 체크할 수 있다).

자기자신에게 signal을 보낼 때는 간단히 raise()를 사용한다(like, int raise(int signo)). 이것은 kill(getpid(), signo) 와 같다. 전체 프로세스 그룹에 보내는 경우에는 killpg(int pgrp, int signo)를 사용한다(kill을 사용해서 kill(int -pgrp, signo)처럼 해도된다). pgrp = 0 이면 자신이 띄운 프로세스들에 모두 보낸다.

Signals, brief

Signal은 비동기적인 이벤트를 핸들링하는 매커니즘을 제공하는 Software Interrupt이다. Signal은 IPC의 원시적인 형태라고 봐도 된다. Unix의 역사상 Signal의 구현은 여러 갈래로 나누어 지려 했으나, 다행히 POSIX에서 표준을 제공하고, 이를 Linux에서 제공하고 있다.

일단 Signal이 발생하면 커널은 전달 가능한 상태가 될 때까지 기다렸다가 이를 해당 프로세스에 전달해준다. Signal의 처리는 아래 세가지 방식으로 진행된다.

  • 무시한다.

  • 잡아서 처리한다.

  • Default로 처리되게 둔다.


Signal 관련 함수들은 signal.h 에 정의되어 있고, kill()함수를 통해 보낼 수 있다. 쉘에서도 kill 을 통해 보낼 수 있으며, -l옵션으로 그 종류를 볼 수 있다. 각각의 signal을 간략히 정리해보면,

SIGABRT 코어내고 종료. assert(), abort() 호출시 이 signal 발생

SIGALRM alarm(), setitimer() with ITIMER_REAL

SIGBUS 이전에는 비메모리 시스템 에러였는데, 지금은 mmap()안된 영역 접근시 발생한다.

SIGCHLD default로는 무시되나, wait() 호출시 child 종료되면 발생

SIGCONT 프로세스 중단 후 재시작시 발생. 터미널, 에디터에서 refresh위해 사용함

SIGFPE floating point 관련 에러

SIGHUP 세션 종료시 발생. 많은 daemon 프로그램에서 이를 사용해서 conf 파일을 다시 읽게 함(convention) 예. apache에서 SIGHUP 받으면 httpd.conf 읽음

SIGILL illegal machine instruction

SIGINT Ctrl-C

SIGKILL

SIGPIPE PIPE 만들어 쓰고 있는데, 받는쪽이 종료한 경우 발생. default로는 종료됨

SIGPROF profiling timer expires (setitimer() with ITIMER_PROF)

SIGPWR low-bat, UPS system -> init process -> system terminate

SIGQUIT Ctrl-\ 모든 foreground 프로세스에게 전달. (코어 후 종료)

SIGSEGV Segmentation Violation

SIGSTOP kill에의해서만 보내질 수 있음. (무시안됨)

SIGSYS invalid system call (최신 바이너리를 오래된 커널에서 돌리지 않는 이상 거의 일어날 일이 없음)

SIGTERM 바로 종료 (이걸 catch 하면 manner 없음 ㅡ.ㅡ;)

SIGTRAP when it cross breakpoint

SIGTSTP Ctrl-z

SIGTTIN, SIGTTOU bg job 들이 읽거나 쓰려고 하는 경우 발생

SIGURG 소켓에 OOB(out of band) 데이터 들어오면 발생

SIGUSR1 SIGUSR2 user-defined purpose

SIGVTALRM setitimer() 관련 signal

SIGWINCH terminal window 값이 바뀐 경우 발생(예, top해놓고 터미널 사이즈를 조절할 때 화면 refresh를 위해 발생).

SIGXCPU 프로세스가 soft processor limit를 초과할 때 발생 (1초에 한번씩 계속.. hard limit까지 넘으면, SIGKILL받고 죽음)

SIGXFSZ process가 file size limit을 넘을 때 발생

Friday, May 9, 2008

Memory, Opportunistic Allocation

linux는 opportunistic allocation을 사용한다. 프로세스가 메모리 할당을 추가로 요구하면 바로 commit 해주지만 실제로 할당은 안되어 있다. 프로세스가 할당되어 있다고 생각한 이 영역에 뭔가를 쓰려고 하면 그때야 비로소 메모리를 할당해주는데 여기에는 demand paging, cow 등이 사용될 수 있다.

이런식의 메모리 할당은 실제 필요한 메모리만 할당해줄 수 있어 공간 효율적이고, 할당 작업을 실제 필요할 때까지 늦출 수 있으며, 실제 가능한 공간 이상으로 할당 해줄 수도 있다(overcommitment)

경우에 따라서 가용한 메모리 공간 보다 더 큰 영역을 할당해달라고 요청하는 프로세스들도 있는데, 커널에서 이를 수용하는 것을 overcommitment라고 한다. 이렇게 overcommitment되어 잘사용되면 다행이고, 진짜로 실제 메모리 이상을 사용하게 되면 OOM (out of memory)에러를 내어 OOM Killer가 해당 프로세스를 종료시키므로, 전체 동작에는 지장이 없다. overcommit 에도 옵션이 있는데, /proc/sys/vm/overcommit_memory 의 값을 통해 조정할 수 있다 (혹은 sysctl의 vm.overcommit_memory).

값이 0인 경우 휴리스틱한 overcommitment를 허용해주고, 1인경우 모든 요청을 허용해준다. 2인 경우에는 overcommitment를 disable하고 strict accounting을 enable하는데, 메모리 할당이 스왑영역 + 물리적 메모리의 일정 비율부분 까지로 제한된다. 일정 비율 부분은 /proc/sys/vm/overcommit_ratio에 정의되어 있는데, 이 값이 50인경우(default) 할당 가능한 메모리 공간은 스왑크기 + 물리적 메모리 크기의 50% 이다.

OOM Killer에 의해 프로세스가 죽는 것을 원치 않기 때문에 strict accounting을 하려고 하지만, 이게 만병통치약은 아니다. ( 가상 메모리는 대부분의 프로세스가 필요이상의 메모리를 할당하여 동작하려한다는 패턴에 기초하여 제공되는 메커니즘인데, 이를 사용하지 않겠다는 것이기 때문이다 )

Memory, Locking

알다시피 Linux는 demang paging(필요하면 그때 swap in 해주고 안쓰면 디스크로 다시 swap out하는) 이다. 그래서 실제 가지고 있는 물리적 메모리 공간 보다 더 큰 공간을 가상으로 제공해준다. 하지만, 경우에 따라서 자신의 메모리가 디스크로 swap out 되면 안되는 경우도 있다.

  • Determinism 특정 메모리에 올라가 있는 루틴이 일정시간내에 반드시 끝나야 하는 경우이다. 디스크에 swap out 되어 있으면 swap in 하는데 걸리는 시간이 상당하므로 정해진 시간에 응답하지 못할 수도 있다.

  • Security암호화된 비밀 정보를 디스크에서 읽어 복호화 한다음 메모리에 가지고 있었는데, 이 영역이 swap out되면 디스크에 복호화된 비밀정보가 남아 보안상의 취약점이 될 수 있다.


그래서 메모리의 일정 부분은 swap out 되지 않도록 잠글 수 있는데, 이 때 사용되는 함수가 mlock() 이다.

#include <sys/mman.h>

int mlock(const void *addr, size_t len);
int mlockall(int flags);

그 외에 특정 프로세스의 모든 부분을 locking 하고싶은 경우에는 mlockall()을 사용하면 된다. mlockall()에서 사용되는 flag으로는 아래 두가지가 있는데 일반적으로 두개를 다 OR해서 사용한다.

  • MCL_CURRENT 현재 프로세스가 사용하는 모든 메모리를 lock

  • MCL_FUTURE 앞으로 사용할 메모리까지도 lock


lock된 메모리의 해제는 munlock(), munlockall(void)을 통해 진행하면 된다. CAP_IPC_LOCK이 설정된 경우에는 무한정으로 메모리를 잠글 수 있지만, 그렇지 않은 경우에는 RLIMIT_MEMLOCK 에 설정된 바이트 까지만 locking할 수 있다. (default로 32KB 이다)

특정 페이지가 디스크에 있는지, 메모리에 있는지를 확인하는 함수로 mincore()가 있다. 아래와 같은데, 이 함수는 bit vector 형식으로 페이지의 메모리/디스크 상주 여부를 리턴해준다.

#include <sys/mman.h>
#include <unistd.h>

int mincore(void *start,
size_t length,
unsigned char *vec);

안따깝게도 이 함수는 MAP_SHARED 옵션으로 mmap 맵핑된(실제 파일에) 영역에 대해서만 정상적으로 동작한다.