Monday, April 14, 2008

Real-time Systems

특정 외부요인이 발현된 시점에서 응답이 있기까지의 시간에 대해 deadline이 제한된 상황에서 동작하는 시스템을 realtime system이라 할 수 있다. 책에서는 soft/hard realtime system으로 나누는데, deadline을 넘겨도 system failure가 발생하지 않는 경우는 soft realtime system 으로 보면 된다.

Latency & Jitter 두 단어에 대하 잠깐 짚고 넘어 가면, Latency는 외부요인이 발생한 시점에서부터 response가 일어나기 전까지의 시간이고, jitter는 연속적인 이벤트들 사이의 시간 편차이다.

Linux에서도 realtime 을 지원하는데, scheduling class 라고 불리는 스케쥴링 정책을 통해서 이루어진다. 세가지 정책이 있는데 flag으로는 SCHED_FIFO, SCHED_RR, SCHED_OTHER로 구분된다.

FIFO Class는 더 높은 우선순위를 가진 프로세스가 들어오기 전까지 FIFO순으로 처리하는 방식이고,

Round-robin 은 같은 우선순위를 가진 프로세스들 간에 RR방식으로 timeslice를 가지고 실행되는 스케쥴링 방식이다.

SCHED_OTHER는 일반적인 스케쥴링 정책이다. 이외에 SCHED_BATCH도 있는데, 다른말로 idle scheduling policy라고도 불린다. 이 정책은 runnable한 프로세스가 없을 때 실행되는 방식으로 realtime 과는 반대 개념으로 동작한다. 스케쥴링을 설정하거나 확인할 때는 sched_getscheduler(), sched_setscheduler()를 사용한다. RR 에서 timeslice 값을 확인하고 싶은 경우에는 sched_rr_get_interval()함수를 사용하면 된다.

Real-time Process를 구현할 때 주의할 사항들이 있는데, 요약하면 아래와 같다고 한다.

  • 무한 루프를 돌 경우 시스템이 응답하지 않게 될 수도 있다.

  • 한 프로세스로 인해 다른 프로세스가 starve하지 않도록 주의해야한다.

  • 자신보다 낮은 우선순위의 프로세스를 busy-wait하는 경우를 주의해야한다(deadlock될수 있음)

  • 개발 시에는 터미널 프로세스를 최상위에 두면 좋다.

  • chrt라는 util-linux 패키지를 쓰면 도움이 된다


Realtime 시스템에서는 생각할 게 많다. 미사일 요격 시스템을 만들어서 잘 동작하다가, 갑자기 중요한 코드를 수행하는데 예를들어 ABM(Anti-Ballistic Missile)를 발사하려는 시점에서 갑자기 해당 코드가 하드디스크에 swap되어 있다고 가정하자. 하드에서 읽어와 메모리에 다시 올리는 순간, 이미 미사일이 나아들어 기지를 덮칠 수도 있다 ㅡ.ㅡ;... realtime 에서 동작하는 프로세스라면 해당 메모리는 swap out되지 않도록 lock해 놓을 필요가 있다.

CPU Affinity를 통해서 realtime process를 설정하는 방법도 있다. multiprocessor를 가진 시스템에서 realtime process를 통작시킨다고 해보자. 이 경우에 모든 일반 프로세스를 한 프로세서에서 동작시키고 중요한 rt 프로세스를 다른 프로세서에서 동작시키면 dealine 안에 job을 마치도록 안전하게 설정 시켜 놓을 수 있다. 간단하게 init 프로세스의 CPU affinity를 한쪽으로 고정시키면 이로부터 fork되는 프로세스들도 같은 affinity를 가지므로 한쪽에서만 돈다. 그리고 원하는 RT 프로세스를 다른 CPU에서 돌도록 하면 된다. 샘플코드를 보자.

// #### init process
cpu_set_t set;
int ret;

CPU_ZERO(&set);
ret = sched_getaffinity(0, sizeof(cpu_set_t), &set);
//check error
CPU_CLR(1, &set); // cpu1에서 못돌게함
ret = sched_setaffinity(0, sizeof(cpu_oset_t), &set);
// check error

// #### real-time process
cpu_set_t set;
int ret;

CPU_ZERO(&set)l
CPU_SET(1, &set);
ret = sched_setaffinity(0, sizeof(cpu_oset_t), &set);
// check error

Tuesday, April 8, 2008

Processor Affinity

SMP 같이 여러 프로세서를 사용하는 경우, 프로세서를 바꿀 때 마다 cache 효과가 사라지는 단점이 있다. (오늘날의 SMP에서는 프로세서간에 Cache 접근이 가능하도록 인텔에서 설계하나, 커널 단에서 이를 고려하고 있는지는 모르겠다.) 그래서 스케쥴러는 되도록 이면 특정 프로세스를 특정 프로세서에서 동작시켜려고 하는 경향이 있는데 이를 'soft affinity' 라고 한다. 이 경향은 프로세서가 아주 imbalance해질 때 풀어지는데, 코드 상에서 특정 프로세서에서만 실행되도록 한다면 이는 'hard affinity' 가 된다. 몇개의 매크로와 sched_setaffinity(), sched_getaffinity()를 사용하여 hard affinity를 줄 수 있는데, 귀찮다... 소스하나만 보고 가자.

cpu_set_t set;
int ret, i;

CPU_ZERO (&set);
CPU_SET (0, &set); // CPU0을 선택
CPU_CLR (1, &set); // CPU1을 배제
// 첫째 인자는 pid
ret = sched_setaffinity(0, sizeof(cpu_set_t), &set);

// 이제 출력해 보면,
// (CPU_SETSIZE는 한 프로세서가 최대로
// 가질 수 있는 프로세서 수이다.)
for (i=0; i<CPU_SETSIZE; i++)
{
int cpu;
cpu = CPI_ISSET(i, &set);
printf ("cpu=%i is %s\n", i,
cpu ? "set" : "unset");
}

Process Priorities

프로세스의 우선 순위는 nice 라고 불리는 값으로 우선 순위가 결정된다. 다른 프로세스 들에게 "be-nice" 하자는 의미에서 우선순위 값이 nice로 정해졌덴다. -20에서 19까지 가질 수 있고, 낮을 수록 우선 순위가 높다. nice 값 자체가 프로세스의 timeslice를 알려주는 역할도 한다. 디폴트 nice 값은 0이다. 이 값을 올려주는 것이 다른 프로세스들에게는 "nice" 해보는 방법이다. nice는 아래와 같이 간단히 사용할 수 있다.

#include <unistd.h>

int nice(int inc);

inc 값만큼 nice 값을 올릴 수 있다. 내릴 수는 없다. 내리는 것 - 우선순위를 높이는 것은 (CAP_SYS_NICE capability가 있는)root만 할 수 있다. 리턴값으로 변환후의 우선 순위 값을 반환하기 때문에 간단히 현재 자신의 우선 순위를 볼 때는 nice(0)으로 확인 가능하다.

이 외에 프로세스, 프로세스 그룹, user 별로 우선순위를 지정할 수 있는 getpriority, setpriority함수들이 있는데, 간단히 살피면 아래와 같다.

#include <sys/time.h>
#include <sys/resource.h>
/* which - PRIO_PROCESS, PRIO_PGRP, PRIO_USER
* who - pid, pgid, user id..
*/
int getpriority (int which, int who);
int setpriority (int which, int who, int prio);

위에서 인자에 0을 주면 현재 프로세스/프로세스 그룹/사용자에 대해 동작한다. 비슷한 식으로 I/O 스케쥴러에 대해서도 우선순위를 조절하는 ioprio_get(...), ioprio_set(..)가 있으나 glibc에서 제공하지 않는다. 대신 ionice 라는 util-linux package를 통해 조절할 수 있다.

Monday, April 7, 2008

Advanced Process Management - process scheduling

프로세스 스케쥴링에서 멀티테스킹을 지원하는 경우 아래의 두가지 방법이 있다.
  • preemtive multitasking(선점형)
  • cooperatice multitasking

cooperative 멀티테스킹에서는 프로세서가 스스로 yield (다른 프로세스로 스위치 하도록) 하지 않는 이상 계속적으로 프로세서 자원을 사용할 수 있다. preemptive에서는 커널이 '선점형' 이기 때문에 특정 프로세스가 일정 timeslice이상을 사용하지 못하도록 제어할 수 있다.

linux의 scheduling algorithm은 O(1)이다. 즉 프로세스의 수가 늘어나도 스케쥴링을 위해 소요되는 시간은 일정하다.

Timeslice 는 너무길거나 짧을 경우 둘다 문제가 된다. 너무 짧을 경우 context switch 하는 비용이 많아지고, 캐시의 temporal locality가 떨어진다. 너무 길게 되면 User Interactivity가 떨어지게 된다.

Processor bound & I/O bound. 프로세스가 할당된 timeslice를 거의 다 사용하는 경우 processor bound 이고, 대부분 파일, 키보드 입력, 마우스 등의 I/O작업인 경우 I/O bound 이다. 전자의 경우 캐시 hit를 늘이기 위해 되도록 많은 timeslice를 사용하여 작업을 마치기 원하고, 후자의 경우에는 짧게 짧게 작업이 이루어지는데 스케쥴러가 빨리 다시 작업을 시작해주도록 해주기를 원한다. Linux Scheduler는 프로세서의 각각 특징에 따라 다른 방식으로 프로세스들을 다루려고 노력한다(주로 IO bound에 대해서는 우선 순위를 높여주고, processor bound에는 penalty를 준다).

Idle Process는 실제 프로세스가 아니다. Idle process는 커널이 자신의 scheduler algorithm을 재조정하는 시간이다. idle time은 이러한 idle process를 돌리는데 소용된 시간이다.

Thread에 대해서 잠깐 생각해보면, Linux에서는 Thread를 같은 주소 공간을 공유하는 프로세스로 간주한다(Thread라고 별다르게 생각하지 않는다). 대부분의 개발자들은 thread 프로그래밍을 위해 IEEE에서 제정한 thread API인 API pthreads를 사용한다.

Yielding processor 란 '나 끝났어, 딴 프로세스 돌려' 라고 알려주는 것이다. 말그대로 프로세서를 양보하는 것인데, 아래 함수를 통해 할 수 있다(별로 일반적이지는 않다).



#include <sched.h>

int sched_yield(void);

이 system call을 사용해서 일종의 non-block한 코드를 구현할 수도 있겠지만, 그리 좋은 방법은 아니다(그냥 non-block 을 지원하는 함수를 쓰는 편이 낫다). 이전에 thread를 구현할 때 user space에서의 thread locking을 구현하기 위해 잠시 사용하기도 했으나, 근래에는 futex로 user space locking이 지원되고 있다.

프로세서가 서로 yield를 해서 ping pong pathological case가 발생할 수 있다. 하지만, 2.6이후 버전에서 yield 알고리즘을 개선하여 이런 문제를 해결하였다.

Friday, April 4, 2008

Daemons

Daemon은 background에서 동작하고, terminal에 연결되지 않은 상태로 동작하는 프로세스를 의미한다. 통상 'd'로 끝나는 이름을 가지고, 아래와 같은 순서로 생성할 수 있다.
  1. fork()
  2. 부모 프로세스는 exit()
  3. setsid()로 새로운 세션을 가진다. (예전 세션이나 pgid로 종료 signal을 받을 수 있기 때문)
  4. chdir()로 작업 디렉토리를 '/'로 바꾼다. (엉뚱한 데를 물고 있다 unmount할 때 문제를 만드는 수가 있다.)
  5. 부모로 부터 받은 fd를 모두 닫는다.
  6. 0,1,2 (in,out,err) I/O를 /dev/null에 연결한다.

이를 기반으로 daemon을 만들 수 있으나, 귀찮다. 그래서 daemon() 함수를 제공하고 있다.



#include <unistd.h>

int daemon(int nochdir, int noclose);

nochdir가 0이 아니면 dir path를 안바꾸고, noclose가 0이 아니면 fd를 안 닫는다.

Sessions and Process groups

간단히 얘기하면 한 시스템에 여러개의 세션이 존재할 수 있고, 세션은 job control의 단위인 여러개의 프로세스 그룹으로 나눠질 수 있는데, 세션과 프로세스 그룹에는 각각 리더가 존재해서 이를 세션 리더, 프로세스 그룹 리더라고 한다. 프로세스 그룹은 foreground/background 로 나누어 진다. 사용자가 터미널을 종료하면 SIGQUIT이, 네트웍이 끊어지면 SIGHUP가, Ctrl-C를 누르면 SIGINT가 foreground process group에 전달된다. 사용자가 세션을 시작하면 그 사용자의 pid가 pgid, sid로 되어 세션 및 프로세스 그룹이 생성된다. 새로운 새션을 시작하기 위해서는 setsid()를 사용하는데, 이를 호출하는 프로세스는 다른 프로세스 그룹의 리더여서는 안된다. 이를 위해 일반적으로 fork()한 후 부모는 종료하고, 자식 르로세스에서 setsid()를 수행해서 새로운 세션을 만든다.

pid_t pid;

pid = fork();

if (pid == -1) perror("fork");
else if (pid !=0) exit(EXIT_SUCCESS);

if (setsid() == -1)
{
perror("setsid");
return -1;
}

현재 세션 id는 비슷하게 getsid()를 통해서 볼 수 있다.(인자에 pid를 주면 다른 프로세스의 sid도 볼 수 있다.) sid에 대한 함수들 같이 pgid도 같은 식으로 set, get할 수 있는데 setpgid(), getpgid()를 통해 볼 수 있다. 리눅스에서는 한 때 이러한 함수들을 setpgrp(), getpgrp()로 제공했었다.

Thursday, April 3, 2008

Process User IDs

user ID에는 네가지가 존재한다. real, effective, saved, filesystem 이다. real은 부모 프로세스의 user ID 이고, effective ID는 현재 프로세스의 user ID이다. fork하면 effective ID가 상속되고, exec해도 setuid 같은 경우를 제외하고는 안바뀐다. setuid 같은 경우란 특정 프로그램을 실행하는 경우 그 프로그램 바이너리의 user ID로 effective ID가 바뀌어 사용되는 경우이다. 예를 들어 /usr/bin/passwd는 root 권한으로 동작한다. 그래서 user가 passwd를 실행하면 그 실행 프로세스는 user ID에서 root ID 권한으로 바뀌어 동작한다는 얘기다. saved ID는 exec으로 권한이 바뀐 경우 그 전의 ID를 적어놓는 것이고, 프로세스가 fork하면 saved ID권한을 자식에게 상속한다.