스핀락, 뮤텍스, 세마포어

컴퓨터공부/리눅스 2018.11.07 20:40 Posted by 아는 개발자

여러 개의 프로세스가 동시에 실행 할 수 있는 멀티 코어 환경은 사용자의 시스템 전반의 성능을 향상 시켜 주었지만 개발자들에게는 '프로세스간 공유 자원 접근 관리'라는 골치아픈 숙제를 남겼다. 다수의 컴포넌트가 공유중인 자원을 동시에 읽거나 수정할 때 생기는 문제들을 포괄해서 '동기화 문제'라고 하며 대부분의 소프트웨어는 스핀락, 뮤텍스, 세마포어라는 자료구조들을 이용해 해결 한다. 이번 포스트에서는 각 자료구조들의 작동 원리와 차이점에 대해서 정리해보려고한다.


스핀락 (spinlock)


특정한 자료구조를 획득(lock) 또는 해제(unlock) 함으로서 공유 데이터에 대한 접근 권한을 관리하는 방법이다. 권한을 획득하기 전까지 CPU는 무의미한 코드를 수행하는 busy waiting 상태로 대기하고 있다가 접근 권한을 얻으면 내부 코드를 수행하고 종료후 권한을 포기한다. 상태가 획득/해제 밖에 없기 때문에 공유 영역에는 하나의 컴포넌트만 접근 할 수 있으며 획득과 해제의 주체는 동일해야한다.


// arch/arm64/kernel/debug-monitors.c
static DEFINE_SPINLOCK(step_hook_lock);

void register_step_hook(struct step_hook *hook)
{
    spin_lock(&step_hook_lock);
    list_add_rcu(&hook->node, &step_hook);
    spin_unlock(&step_hook_lock);
}

CPU를 선점하고 있는 busy waiting 상태로 대기하기 때문에 권한이 해제되는 대로 빨리 작업을 수행할 수 있는 장점이 있지만 선점 기간동안 다른 프로세스 작업이 지연될 수 있는 오버헤드도 존재한다. 그래서 짧게 수행할 수 있는 작업에 주로 사용된다.


뮤텍스 (mutex)


획득(lock), 해제(unlock) 두가지  상태가 존재해 하나의 컴포넌트만 공유영역에 접근 할 수 있고 획득과 해제의 주체가 동일해야한다는 점은 스핀락과 동일하나 권한을 획득 할 때 까지 busy waiting 상태에 머무르지 않고 sleep 상태로 들어가고 wakeup 되면 다시 권한 획득을 시도한다. 시스템 전반의 성능에 영향을 주고 싶지 않고 길게 처리해야하는 작업인 경우에 주로 사용한다. 주로 쓰레드 작업에서 많이 사용된다.


 
// arch/arm64/mm/dma-mapping.c
mutex_lock(&iommu_dma_notifier_lock);
list_for_each_entry_safe(master, tmp, &iommu_dma_masters, list) {
   if (data == master->dev && do_iommu_attach(master->dev,
        master->ops, master->dma_base, master->size)) {
    list_del(&master->list);
    kfree(master);
    break;
   }
}
mutex_unlock(&iommu_dma_notifier_lock);


세마포어 (Semaphore) 


스핀락, 뮤텍스와는 다르게 표현형이 정수형이며 이점을 살려 하나 이상의 컴포넌트가 공유자원에 접근하도록 허용할 수 있다. 예로 들면 뮤텍스와 스핀락은 옷가게에 한 개의 피팅룸만 있었던 반면 세마포어는 하나 이상의 피팅룸이존재하는 셈. 물론 세마포어를 이진수의 형태로 사용해 개념적으로 뮤텍스와 스핀락처럼 사용하는 것도 가능하다. 정수형인 만큼 획득과 해제 같은 명령이 아니라 값을 올리고 줄이는 방식으로 세마포어를 사용하다. 세마포어의 값이 0이면 기다려야 한다.


// fs/btrfs/disk-io.c
    down_read(&fs_info->cleanup_work_sem);
    if ((ret = btrfs_orphan_cleanup(fs_info->fs_root)) ||
        (ret = btrfs_orphan_cleanup(fs_info->tree_root))) {
        up_read(&fs_info->cleanup_work_sem);
        close_ctree(tree_root);
        return ret; 
    }    
    up_read(&fs_info->cleanup_work_sem);


스핀락과 뮤텍스와 달리 세마포어는 해제(Unlock)의 주체가 획득(Lock)과 같지 않아도 된다. 즉 어떤 프로세스가 세마포어의 값을 감소시켜도 다른 프로세스가 풀어줄 수 있다. 이 특징을 고려해 세마포어를 시그널(Signal) 원리로 사용 할 수도 있다.


아래의 그림처럼 TASK A가 먼저 스케줄링돼 나중에 해야할 작업(post_work)을 먼저 할 수도 있는 상황이라고 해보자. 초기 세마포어의 값을 0으로 초기화해 post_work를 호출하지 못하게 막는다. 그러고 난 후 TASK B의 pre_work가 끝난 후에 세마포어의 값을 올려서 TASK A가 post_work를 호출할 수 있도록 할 수 있다.




참고자료


- FEABAHAS 블로그

'컴퓨터공부 > 리눅스' 카테고리의 다른 글

스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18

RCU (Read-Copy Update)

컴퓨터공부/리눅스 2018.10.30 21:41 Posted by 아는 개발자

공유중인 값을 읽는 쓰레드와 업데이트하는 쓰레의 개수가 각각 하나인 경우에는 경우에는 spin_lock을 이용해도 별 어려움 없이 동기화 작업을 수행 할 수 있다. 그런데 값을 읽는 프로세스가 두개 이상이 될 때부터는 동기화 작업이 골치가 아파진다. 아래의 코드처럼 두개 이상의 쓰레드가 공유중인 val 값을 읽으려고 하고 한개의 쓰레드가 값을 업데이트 하고 있다고 해보자. 본래의 목적은 reader를 호출하는 thd[0]와 thd[1] 쓰레드가 writer를 호출하는 thd[2] 쓰레드가 업데이트 하는 값을 동일하게 읽는 것이었다.


reader 두개를 writer보다 앞서서 생성한 덕분에 초반에는 어느 정도 동기화가 유지가 되겠지만 시간이 지나면서 쓰레드의 priority 값이 달라지게되고 어떤 기점에서 thd[2] 쓰레드가 thd[0]와 thd[1] 사이에 실행될 수도 있다. 이런 경우 thd[0]와 thd[1]은 같은 값을 읽지못하게 된다.


pthread_spinlock_t lock;
int val;

void *reader() {
    int read;
    while (1) {
        pthread_spin_lock(&lock);
        read = val;
        printf("reader1: %d\n", val);
        pthread_spin_unlock(&lock);
    }
}

void *writer() {
    while(1) {
        pthread_spin_lock(&lock);
        val++;
        pthread_spin_unlock(&lock);
    }
}

int main() {
    pthread_t thd[3];

    pthread_spin_init(&lock, PTHREAD_PROCESS_SHARED);
    pthread_create(&thd[0], NULL, reader, NULL);
    pthread_create(&thd[1], NULL, reader, NULL);
    pthread_create(&thd[2], NULL, writer, NULL);

    pthread_join(thd[0], NULL);
    pthread_join(thd[1], NULL);
    pthread_join(thd[2], NULL);

    printf("DDD\n");

    return 1;
}


이런 경우를 위해 리눅스에서는 RCU(Read-Copy Update)라는 라이브러리를 제공한다. RCU를 사용하면 여러 개의 읽기 작업과 한 개의 쓰기 작업의 동기화를 구현 할 수 있다.  단, 여러 개의 쓰기 작업의 동기화는 지원이 안된다. LWN에서 소개한 예시코드로 간단히 사용 방법을 익혀보자.


 struct foo {
   struct list_head list;
   int a;
   int b;
   int c;
 };
 LIST_HEAD(head);

 /* . . . */

 p = kmalloc(sizeof(*p), GFP_KERNEL);
 p->a = 1;
 p->b = 2;
 p->c = 3;
 list_add_rcu(&p->list, &head);

// Thread1
 rcu_read_lock();
 list_for_each_entry_rcu(p, head, list) {
   do_something_with(p->a, p->b, p->c);
 }
 rcu_read_unlock();

// Thread2
 q = kmalloc(sizeof(*p), GFP_KERNEL);
 *q = *p;
 q->b = 2;
 q->c = 3;
 list_replace_rcu(&p->list, &q->list);
 synchronize_rcu();
 kfree(p);


11~15 라인에서는 `foo`구조체 포인터로 선언된 `p`변수에 메모리 공간을 할당하고 값을 대입한 후 `list_add_rcu`라는 함수를 이용해 리스트에 넣는 작업이다. `list_head` 변수가 확장성이 좋아 RCU에서도 list를 이용해 여러 개의 변수를 관리할 수 있도록 했다.


Thread1 은 읽는 작업을 수행하는 reader다. `p`값을 읽어오기 전에 `rcu_read_lock()` 함수를 호출해서 접근 권한을 얻고 작업을 수행한 다음에는 `rcu_read_unlock()`으로 lock을 풀어준다. 다른 쓰레드도 읽는 작업을 수행한다면 위와 동일한 코드를 탄다.


Thread2는 rcu 리스트의 값을 업데이트하는 쓰레드다. `list_replace_rcu()` 함수는 첫번째 인자의 값을 두번째 인자의 값으로 바꾸는 함수다. 그리고 바로 아래 `synchronize_rcu()`는 다른 쓰레드에서 rcu 리스트의 값을 읽는 작업이 모두 종료될 때 까지 기다린 후 수정한 정보를 업데이트 한다. 이 함수가 호출되지 않으면 RCU 리스트 수정 함수를 호출해도 reader쪽에서 읽는 값은 동일하다. 이 함수를 이용해 읽는 작업과 쓰는 작업의 동기화를 맞춰 줄 수 있다.


* 더 상세한 동작 메커니즘을 알고 싶으신 분은 LWN 을 참조하면 좋을 것 같다.

'컴퓨터공부 > 리눅스' 카테고리의 다른 글

스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18

Cgroup (Control Group)

컴퓨터공부/리눅스 2018.09.15 13:37 Posted by 아는 개발자

Cgroup (Control Group)


Cgroup은 CPU, Network, Memory 등 하드웨어 자원을 그룹별로 관리 할 수 있는 리눅스의 모듈이다. 하나 또는 복수의 장치를 묶어서 하나의 그룹을 만들 수 있으며 개별 그룹은 시스템에서 설정한 값만큼 하드웨어를 사용할 수 있다. 설정하는 값은 예를 들면 이런 것들이다.

  • 어떤 그룹이 CPU를 더 많이 차지 할 것인지?
  • 그룹이 얼마만큼 메모리를 사용 할 수 있는지?
  • 네트워크 우선순위를 얼만큼 줄 것인가.
시스템에 생성된 프로세스들은 장치 별로 특정한 cgroup에 속하며 프로세스가 사용하는 하드웨어 자원의 총량은 속한 cgroup의 통제를 받게 된다. 이말은 곧 프로세스가 아무리 효율적으로 동작하도록 만들어져 있어도 cpu 점유율이 낮은 cgroup에 속해 있으면 속도가 느릴 수 밖에 없고 cgroup 자체가 CPU 점유율이 높아도 이 그룹에 속한 프로세스가 많으면 전반적인 속도가 저하 될 수 밖에없다. 리눅스에선 하드웨어와 프로세스 사이에 cgroup 계층을 두어서 자원을 관리할 수 있도록 만들었다.

하드웨어와 cgroup, 프로세스간의 관계는 아래의 그림처럼 표현 할 수 있다.



시스템에 어떤 cgroup이 있는지 그리고 프로세스가 어떤 cgroup에 매핑되어 있는지는 리눅스 파일 시스템에 모두 매핑 되어 있다. 간단히 cat과 ls 명령어로 조회가 가능하다.


// System에 설치된 cgroup 목록
kwony@kwony:~$ ls /sys/fs/cgroup/
blkio      cpuacct      devices  memory            net_prio    systemd
cgmanager  cpu,cpuacct  freezer  net_cls           perf_event
cpu        cpuset       hugetlb  net_cls,net_prio  pids

// 프로세스별 cgroup 정보. /proc/[pid]/cgroup
kwony@kwony:~$ cat /proc/8502/cgroup 
11:pids:/user.slice/user-1000.slice
10:cpuset:/
9:net_cls,net_prio:/
8:devices:/user.slice
7:freezer:/
6:perf_event:/
5:hugetlb:/
4:cpu,cpuacct:/
3:blkio:/
2:memory:/
1:name=systemd:/user.slice/user-1000.slice/session-c2.scope


'컴퓨터공부 > 리눅스' 카테고리의 다른 글

스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18

CPU pinning과 taskset

컴퓨터공부/리눅스 2018.08.27 22:49 Posted by 아는 개발자

Big/Little로 이루어진 멀티코어 하드웨어 환경에서는 소프트웨어의 성능을 높이기 위해 하드웨어적인 트릭을 사용하는데 이중 가장 대표적인 것이 프로세스를 처리할 수 있는 CPU 종류를 설정하는 CPU pinning 방법이다. CPU affinity라고도 부른다.


기본적으로 스케줄러는 큐에 대기중인 프로세스를 하드웨어상에서 idle 상태에 있는 CPU에 최우선적으로 작업을 할당한다. 그런데 idle 상태에 있는 CPU가 Little라면 처리하는 속도가 Big Core에 비해서 확연히 차이가 날 수 있다. 칩에 따라서 다르지만 일반적으로 Big Core와 Little Core의 성능은 1.5배정도 차이가 난다. 만약 오래 걸리는 작업이라면 Little Core에 할당하는 것보다 Big Core가 idle이 되길 기다렸다가 여기서 처리하도록 하는 것이 성능적으로 우수 할 수 있다.


물론 시스템 내에 존재하는 모든 프로세스가 Big Core에서만 동작하려고 한다면 성능의 효과를 보지 못한다. CPU pinning은 시스템 전반의 효율화를 위해 만들어진 스케줄러의 빈틈을 노리는 것이기 때문에 다른 프로세스들은 모든 코어에서 정상적으로 돌고 있는 환경에서 효과를 극대화 할 수 있다. 그래서 일반적인 소프트웨어에서는 사용하지 않고 여러 개의 Virtual Machine이 동작하는 환경에서 VM의 성능을 조정할 때 주로 이용된다.



taskset은 Linux 환경에서 CPU pinning을 지원하는 커맨드다. 현재 프로세스에 pinning된 CPU 정보를 볼 수 있고 역으로 설정 할 수도 있다. 사용방법은 아래의 코드로만 봐도 될 정도로 간단하다.

// taskset -pc {pid} : pinning 정보 보기
// taskset -pc {Masking ex) 0-3 or 1,2,3,4} {pid} : pinning 설정하기
kwony@kwony:/proc/4007/task/4007$ taskset -pc 4007
pid 4007's current affinity list: 0-3
kwony@kwony:/proc/4007/task/4007$ taskset -pc 1,2,3 4007
pid 4007's current affinity list: 0-3
pid 4007's new affinity list: 1-3


'컴퓨터공부 > 리눅스' 카테고리의 다른 글

RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18
workqueue 사용법  (0) 2018.07.16

스핀락

컴퓨터공부/리눅스 2018.07.23 22:23 Posted by 아는 개발자

멀티프로세서 환경에서는 시스템에서 생성된 프로세스가 하드웨어에 설치된 CPU의 수만큼 동시에 실행된다. CPU에서 동작 중인 프로세스는 메모리 영역에 할당된 데이터중 연산이 필요한 값을 읽고 쓰는 작업을 수행하게 되는데 이때 여러 개의 프로세스가 같은 데이터 영역을 공유하게 되는 경우가 있다. 여러 개의 프로세스가 공통된 전역 변수의 값을 읽는 경우가 대표적인 예다.




데이터 값이 고정되고 모든 프로세스가 고정된 값을 읽기만 하면 별 다른 문제는 없다. 그런데 누군가가 쓰게 되는 경우부터 싱크 문제가 생긴다. A랑 B 프로세스가 있다고 해보자. A 프로세스는 C와 D의 데이터 값을 수정하게 되어있고, B 프로세스는 A프로세스보다 조금 늦게 생성되어 A 프로세스가 수정한 값을 읽도록 짜여져 있다. 싱글코어에선 A 프로세스 다음에 B 프로세스가 스케줄링돼 수정 작업과 읽는 작업이 순차적으로 이뤄진다. 그런데 멀티코어에서는 B프로세스가 A 프로세스의 작업이 끝나기 전에 스케줄링 될 수도 있다. 이런 경우에는 수정된 C, D 값을 읽지못하고 예전 값을 읽게 될수도 있다.


static __always_inline void spin_lock(spinlock_t *lock)
static __always_inline void spin_unlock(spinlock_t *lock)


이런 경우를 막기위해 리눅스에서는 spin_lock이라는 API를 제공한다. spin_lock은 매개변수(spinlock_t)의 값을 이용해 다른 프로세스의 공유 영역에 대한 접근을 막을 수 있는 함수다. spin_lock을 부르면 매개 변수의 값을 보고 다른 프로세스의 점유 유무를 확인한다. 점유한 상태가 아니면 매개변수의 값을 점유 상태로 바꾸고 공유 영역에서 수행할 작업을 수행하고 그렇지 않으면 다른 프로세스가 놓을 때까지 (busy waiting으로)기다린다. 수행한 작업을 완료한 후 spin_unlock 함수를 통해 점유 상태를 취소한다. 취소 후에는 대기중이던 프로세스가 매개 변수의 값을 읽고 재점유한다.


다시 예제로 돌아와보자. A 프로세스가 값을 수정하기 전에 B 프로세스가 읽는 것을 막기 위해선 값을 수정하는 A 프로세스가 값을 수정하는 작업과 B 프로세스가 값을 읽는 작업 앞에 spin_lock을 걸어두면 된다. 수정 작업이 먼저 실행되기 때문에 A 프로세스가 먼저 lock을 획득하게 되고 중간에 읽기 작업을 수행하는 B 프로세스는 lock을 획득하지 못해 busy waiting 상태로 기다린다. 수정작업이 모두 끝난 A는 다른 프로세스가 접근 할 수 있도록 spin_unlock을 수행하게 되고 이 이후에 B 프로세스는 lock을 획득해 수정된 값을 읽을 수 있게 된다.




여기서 더 나아가 하드웨어에서 인터럽트가 들어온 상황을 생각해보자. 프로세스가 lock을 획득해서 작업하는 도중에 인터럽트가 들어오면 CPU는 하던일을 중단하고 인터럽트 서비스 루틴을 처리한다. 그런데 만약 인터럽트를 처리하는 작업중에서 인터럽트가 들어오기전 프로세스가 획득한 lock을 갖기 위해 spin_lock을 부르는 코드가 있다고 해보자. 인터럽트 서비스 루틴도 일반 프로세스와 마찬가지로 lock을 획득 할 수 있을 때까지 대기 상태에 있다. 그런데 lock을 놓아줘야하는 프로세스는 여전히 인터럽트에 걸린 상태에서 다시 스케줄링 되지 못하고 있다. 서로가 서로의 진행을 막고있는 셈이다. 이런 경우 프로세스는 교착 상태(Dead Lock)에 빠진다.


static __always_inline void spin_lock_irq(spinlock_t *lock)
static __always_inline void spin_unlock_irq(spinlock_t *lock)


이를 해결하기 위해 리눅스에서는 lock을 획득한 상태에서는 cpu의 인터럽트를 막을 수 있는 함수를 제공한다. spin_lock_irq 함수를 사용하면 해당 lock을 획득하고 다시 놓을 때까지 프로세스의 인터럽트를 막는다. 이 시간 동안 들어오는 인터럽트는 pending 상태로 대기하고 있다가 spin_unlock_irq가 불리면 다시 재실행 된다.


그런데 spin_unlock_irq는 disable 되어있던 인터럽트를 모두 enable 시키는데 이러면 함수의 depth가 깊어지는 경우 문제가 발생 할 수 있다. 어떤 함수가 spin_lock_irq를 부르고 lock 내부 루틴을 타는 함수가 또 spin_lock_irq를 부르면 정상적으로 함수가 종료 될 때 spin_unlock_irq가 두번 불리게 될 것이다. 그런데 spin_unlock_irq는 예전에 인터럽트가 몇번 disable 됐는지 상관없이 모두다 enable하게돼 첫번째 spin_unlock_irq와 두번째 spin_unlock_irq가 호출되는 사이 지점은 인터럽트가 disable이 되지 않는다. 즉 spin_lock_irq를 못쓰고 spin_lock을 쓰는것과 마찬가지인 셈. 이 사이에 하드웨어 인터럽트가 들어와 똑같은 lock에 spin_lock을 걸면 예전처럼 deadlock이 걸린다.


#define spin_lock_irqsave(lock, flags)              \
do {                                \
    raw_spin_lock_irqsave(spinlock_check(lock), flags); \
} while (0)
static __always_inline void spin_unlock_irqrestore(spinlock_t *lock,
                                 unsigned long flags)
{
    raw_spin_unlock_irqrestore(&lock->rlock, flags);
}


이런 경우를 해결하고자 spin_lock_irqsave라는 함수가 있다. 잘 보면 이 함수는 매개변수로 flags라는 값을 하나 더 주는 것을 볼 수 있다. flag에다가 현재 interrupt의 상태를 보존해 놓는 것이다. 이 값을 활용해 더 안전하게 스핀락을 처리할 수 있도록 한다.



'컴퓨터공부 > 리눅스' 카테고리의 다른 글

Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18
workqueue 사용법  (0) 2018.07.16
tasklet 사용법  (0) 2018.06.17

eventfd

컴퓨터공부/리눅스 2018.07.18 21:37 Posted by 아는 개발자

eventfd는 리눅스기반 운영체제에서 파일 디스크립터를 통해 프로세스들끼리 통신 할 수 있는 API중 하나다. IPC로 가장 유명한 pipe는 read와 write용으로 채널을 따로 만드는 것과는 달리 eventfd에서는 읽고 쓸 파일 디스크립터 하나면 충분하기 때문에 번거로운 세팅작업 없이 간단하게 구현 할 수 있다. 그러나 단 한개의 채널을 사용하는만큼 사용할 수 있는 기능은 제한적이다. 이 파일 디스크립터로 교환할 수 있는 값은 1 이상의 64비트 정수 하나가 전부라 데이터 전송보다는 이름처럼 프로세스간에 event를 전달하는 용도로 사용한다.


1. eventfd(uint64 initval, int flag)


프로세스들끼리 통신 할 수 있는 파일 디스크립터를 만드는 함수다. 종료후에는 채널로 사용 할 수 있는 64비트 정수형 파일 디스크립터가 리턴된다.


첫번째 인자는 counter 값이다. counter 값은 eventfd의 고유한 값 정도로 보면 될 것 같다. 이 값은 write로 추가 할 수 있고 read로 읽을 수 있다. 그런데 flag에 따라서 동작이 달라지니까 유의하도록 한다.


2. read(int fd, void* buf, size_t count)


eventfd의 counter 값을 읽는 함수다. 단 counter 값은 무조건 1 이상이 되야 하고 counter 값이 0이면 되면 읽지 못하고 에러를 리턴한다. 1이상이면 가지고 있던 counter 값을 리턴한다. 그런데 리턴한 후에는 flag 값에 따라서 counter 값을 변형한다.


- EFD_SEMAPHORE가 세팅된 경우에는 counter 값을 1만큼 감소시킨다. 세마포어 기능을 한다고 보면 된다. 여러개 프로세스가 읽는 경우를 생각한 것 같다.


- EFT_SEMAPHORE가 세팅이 안된 경우에는 counter 값을 0으로 리셋한다.


3. write(int fd, const void *buf, size_t count)


counter 값을 추가할 수 있는 함수다. counter 값은 uint64 범위 내에서 자유롭게 추가 할 수 있으며 만약 최대값을 넘는 경우에는 막히고 에러를 리턴한다. 별거 어려운게 없다.


나중에는 실제로 프로세스들이 어떻게 사용하고있는지 분석해봐야겠다.




'컴퓨터공부 > 리눅스' 카테고리의 다른 글

CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18
workqueue 사용법  (0) 2018.07.16
tasklet 사용법  (0) 2018.06.17
tasklet과 workqueue의 차이점  (0) 2018.06.15

workqueue 사용법

컴퓨터공부/리눅스 2018.07.16 20:30 Posted by 아는 개발자

workqueue를 사용하는 작업은 크게 queue와 work를 만들고 작업을 스케줄 하는 것으로 나뉜다.


1. Queue 생성


workqueue도 이름에서 짐작 할 수 있듯이 queue이기 때문에 자료구조시간에 지겨울 정도로 봤던 queue를 만들어야 한다. 하지만 push나 pop처럼 queue를 사용하는 함수 API까지 만들어줄 필요는 없고 내가 짓고 싶은 Queue의 이름만 하나만 생각하고 생성 매크로만 호출하면 된다

#define create_workqueue(name)                      \
    alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))

리턴인자는 workqueue_struct * 다. 이 포인터를 이용해서 queue에 넣는 작업을 할 수 있다.


2. Work 생성


Work를 생성할 때는 queue처럼 이름까지는 필요 없고 스케줄 될 때 실행할 함수만 있으면 된다. queue처럼 work도 이미 구현된 매크로를 호출해서 간단히 초기화 할 수 있다.

#define INIT_WORK(_work, _func)                     \
    __INIT_WORK((_work), (_func), 0)

void acpi_scan_table_handler(u32 event, void *table, void *context) {
...
INIT_WORK(&tew->work, acpi_table_events_fn);
}

첫번째 인자는 struct work_struct 자료구조의 포인터이고 두번째 인자는 void로 선언된 함수의 포인터다. 일반 콜백 함수를 등록하는 것과 비슷하다. 그 밑의 코드는 실제로 사용하는 예시다. tew->work인 구조체에다가 acpi_table_events_fn 함수를 호출하도록 했다.


3. 스케줄링


delay해서 실행하려는 작업(work)를 만든 queue에다가 넣는 작업이다. 함수 이름만 봐도 대강 무슨 일을 하는지 짐작 할 수 있다.


static inline bool queue_work(struct workqueue_struct *wq,
                  struct work_struct *work)
extern bool queue_work_on(int cpu, struct workqueue_struct *wq,
            struct work_struct *work);


queue_work는 첫번째 인자로 workqueue_struct의 포인터를 받고 두번째 인자로는 실행할 작업인 work_struct를 받는다. queue에다가 작업을 넣는 함수다. 바로 밑의 함수인 queue_work_on은 cpu라는 인자가 있는데 이 인자를 이용해서 스케줄링할 cpu를 선택 할 수 있다.


queue를 선언하지 않고 work만 선언해서 바로 스케줄 할 수 있다. 이때는 queue를 사용하지 않는건 아니고 시스템 부팅때 초기화된 queue(system_wq로 선언돼있다)를 사용해서 스케줄링 한다. 

static inline bool schedule_work(struct work_struct *work)
static inline bool schedule_work_on(int cpu, struct work_struct *work)



'컴퓨터공부 > 리눅스' 카테고리의 다른 글

스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18
workqueue 사용법  (0) 2018.07.16
tasklet 사용법  (0) 2018.06.17
tasklet과 workqueue의 차이점  (0) 2018.06.15
ARM64 리눅스 부팅 초기 어셈블리 코드 분석(head.S) (2/2)  (0) 2018.01.27

tasklet 사용법

컴퓨터공부/리눅스 2018.06.17 09:48 Posted by 아는 개발자

tasklet을 사용하는 과정은 tasklet을 생성하는 작업과 스케줄하는 작업으로 구분된다.


1. tasklet 생성


include/linux/interrupt.h 라는 파일을 보면 tasklet_struct 라는 구조체가 존재한다. 구조체 내의 속성 값을 입력해서 tasklet이 수행할 작업을 설정 할 수 있는데 일일이 속성 값을 호출할 필요는 없이 tasklet_init 이라는 함수를 이용해서 간단히 선언 할 수 있다.

// include/linux/interrupt.h
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};
// kernel/softirq.c
void tasklet_init(struct tasklet_struct *t, 
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0); 
    t->func = func;
    t->data = data;
}

tasklet_init 함수에서 짐작할 수 있듯이 속성은 다섯개지만 이중에서 사용자가 입력해야 할 값은 *func과 data다. *func는 tasklet이 실행해야하는 함수고 data는 function 실행시 이용 할 수 있는 argument다. data를 일반 상수 값을 전달하기도 하지만 주소 값을 전달해서 포인터로 받아 낼 수도 있다.


아래는 리눅스 드라이버에서 사용하는 예다. *func 함수에서 data 값을 주소로 보고 포인터를 이용해 구조체 값들을 읽는다. 여러 개의 데이터를 전달해야 하는 경우에 사용하면 유용하다.

// linux/driver/firewire/ohci.h
static int ar_context_init(struct ar_context *ctx, struct fw_ohci *ohci,
               unsigned int descriptors_offset, u32 regs)
{
// ...
    ctx->regs        = regs;
    ctx->ohci        = ohci;
    tasklet_init(&ctx->tasklet, ar_context_tasklet, (unsigned long)ctx);
// ...
}
static void ar_context_tasklet(unsigned long data)
{
    struct ar_context *ctx = (struct ar_context *)data;
//...
}

2. tasklet 스케줄


tasklet_schedule, tasklet_hi_schedule이란 함수를 이용해서 앞서 선언한 tasklet 구조체를 실제 CPU에 스케줄링 할 수 있다. 두 함수는 매우 비슷하나 softirq number가 다르다. irq number가 낮을수록 더 우선적으로 처리되니 더 높은 우선적으로 처리하고 싶은 작업이라면 tasklet_hi_schedule을 사용하자

// kernel/softirq.c
void __tasklet_schedule(struct tasklet_struct *t)
{
    // ...
   raise_softirq_irqoff(TASKLET_SOFTIRQ);
}
EXPORT_SYMBOL(__tasklet_schedule);

void __tasklet_hi_schedule(struct tasklet_struct *t)
{
    // ...
   raise_softirq_irqoff(HI_SOFTIRQ);
}
EXPORT_SYMBOL(__tasklet_hi_schedule);

3. kill 또는 disable


tasklet을 이미 스케줄 했는데 사용해선 안되거나, 스케줄링 queue에서 빼야할 때가 있다. queue에는 남겨두나 사용하고 싶지 않은 경우에는 tasklet_disable 함수를 사용하면 되고 반대로 다시 살리고 싶으면 tasklet_enable을 쓰면 된다. 아예 queue에서 빼버리고 싶으면 tasklet_kill 함수를 사용한다.

// include/linux/interrupt.h
static inline void tasklet_disable(struct tasklet_struct *t)
static inline void tasklet_enable(struct tasklet_struct *t)
extern void tasklet_kill(struct tasklet_struct *t);

4. 추가 


- 설명한것 외에도 다른 함수가 있지만 이정도만 알아도 사용하는데는 지장이 없을 것 같다. 이외의 함수들은 드라이버 단에서는 거의 사용하지 않는다.


- 분석하면서 토발즈가 남긴 주석을 읽어봤는데 되도록이면 softirq를 새로 짜지 말고 가능한 tasklet을 사용하라고 한다. 기존 동작 루틴을 망칠까봐 그런건가. 하긴 모든 드라이버들이 너도나도 softirq 새로 추가하겠다고 하면 슬롯이 부족해서 안될 것 같기도 하다.


- 스케줄링 후 콜백 함수가 실행되기 까지 세부 동작 과정이 궁금하신 분은 아래의 링크를 참조하면 좋을 것 같다.


Schicao's NotesIBM Developer 사이트 두 포스트의 내용이 비슷한데.. 누가 누구껄 보고 쓴건지 궁금하다.

tasklet과 workqueue의 차이점

컴퓨터공부/리눅스 2018.06.15 20:04 Posted by 아는 개발자

 커널 내의 코드를 짜다보면은 특정 작업을 다른 CPU에서 처리해야 하기도하고 어떤 작업은 몇미리 후에 처리 할 필요가 있는데 이런 경우 리눅스 커널에서는 tasklet과 workqueue라는 API를 사용해서 간단히 해결 할 수 있다. booklet, piglet 단어처럼 기존보다 작은 단위를 표현 할 때 let을 쓴다는 점으로 추측해볼 때 tasklet은 작은 일을 처리할 때 사용하는것 같고 같은 논리로 workqueue는 접미사로 queue가 있다는 점에서 미루어보아 작업(work)을 queue에 넣어서 처리하는 API인 것 같다. 


tasklet과 workqueue 모두 특정 작업을 미룰 수 있다는 점에선 동일하나 동작하는 매커니즘은 다르다. 먼저 tasklet은 softirq를 이용해서 동작한다. 세부 과정은 이렇다. tasklet이 스케줄 되면 스케줄러는 이 tasklet을 처리할 수 있는 CPU에게 softirq를 날리고 softirq를 받은 CPU는 하던 일을 멈추고 tasklet에서 설정된 작업을 수행한다. interrupt를 받은 상태로 동작하기 때문에 더 우선순위가 높은 interrupt가(ex. HW interrupt) 오지 않는 이상 tasklet의 작업을 모두 수행하기 전까지 다른 프로세스가 끼어들 수 없다. 만약 tasklet에서 처리하는 작업이 길고 남발하면 다른 작업들은 그만큼 뒤로 밀리게 돼 시스템 전반의 성능 저하가 올 수도 있다. 따라서 tasklet을 사용할 때는 가능한 짧게 수행 할 수 있는 작업을 써야한다. 이름을 참 잘지은 것 같다.


workqueue는 일반 프로세스가 스케줄링 되는 것과 비슷한 원리로 동작한다. workqueue를 관리하는 handler는 일반 프로세스처럼 CPU의 스케줄링을 받기 때문에 workqueue는 tasklet과 달리 작업이 끝나지 않았더라도 sleep에 들기도 한다. 작업을 모두 끝내기 전까지 CPU를 독점하는 tasklet에 비해서 처리하는데 시간이 오래 걸리지만(higher latency) 시스템에 무리를 줄 수 있는 요소가 없어 시간을 두고 처리해야하는 작업을 사용할 때 유용하다. 자유도가 더 높은 덕분인지 API의 기능도 tasklet에 비해서 많다.


 

tasklet 

workqueue 

 동작방식

 Interrupt

 Schedule 

 장점

 처리가 빠르다 

 시스템에 무리가 없다 

 단점

 시스템에 무리를 줄 수 있다 

 처리가 느리다 



보다시피 tasklet과 workqueue는 수행하는 일 자체는 동일할 지 모르나 사용하는 상황은 다르다. 짧고 빠르게 수행해야 하는 작업(low latency)일 경우에는 tasklet을 사용해야 하는 반면 긴 시간을 두고 처리해야하는(high latency) 작업인 경우에는 workqueue를 사용하는 것이 좋다. 본인 작업의 성격에 따라서 tasklet과 workqueue를 잘 구분해서 사용하도록 하자.


다음 포스트에서는 tasklet과 workqueue의 사용법에 대해서 다루도록 하겠다.


* tasklet은 버전 2.3에, workqueue는 버전 2.5에 mainline에 포함됐다. 둘다 역사가 오래된 API이다.


* 참고사이트


https://www.ibm.com/developerworks/library/l-tasklets/index.html IBM 블로그에서 정리를 아주 잘해뒀다. 돈내고 읽어야 할 것 같은 글인데.

https://stackoverflow.com/questions/18321858/what-is-the-difference-between-tasklet-and-workqueue

앞 포스팅에서 다루지 못한 부분들을 마저 분석해보자. 처음에는 ARM 어쎔 코드도 생소했고 Exception Level 개념도 없어 많이 헤맸는데 이젠 어느정도 훈련도 되어 있고 앞에서 했던 것들 보다 내용 도 적을 뿐만 아니라 상대적으로 익숙한 작업들이라 쉽다.


4. setup_boot_mode_flag


set_cpu_boot_mode_flag:
	adr_l	x1, __boot_cpu_mode
	cmp	w0, #BOOT_CPU_MODE_EL2
	b.ne	1f
	add	x1, x1, #4
1:	str	w0, [x1]			// This CPU has booted in EL1
	dmb	sy
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret


현재 실행 되고 있는 cpu의 Exception Level Mode를 설정한다. VHE기능이 도입되면서 리눅스 커널도 EL2에서 돌아갈 수 있게 됐는데 그 기능을 위한 코드인 것 같다. __boot_cpu_mode 변수에 값을 대입하는것 외에는 별다른 작업이 없다.


5. __create_page_tables


페이지 테이블을 생성하는 작업이다. 그런데 모든 커널 영역에 대해서 매핑하는건 아니고 MMU가 켜지기 전에 필요한 일부 영역들을 1:1로 매핑한다. 위 영역으로는 idmap 과 kernel image 앞 부분의 몇 MB 영역이 있다. kernel 부팅으로 넘어가기 전에 필요한 최소한의 작업인가보다


	/*
	 * Create the identity mapping.
	 */
	adrp	x0, idmap_pg_dir
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start)
        /* Skip ... */
	create_pgd_entry x0, x3, x5, x6
	mov	x5, x3				// __pa(__idmap_text_start)
	adr_l	x6, __idmap_text_end		// __pa(__idmap_text_end)
	create_block_map x0, x7, x3, x5, x6


	/*
	 * Map the kernel image (starting with PHYS_OFFSET).
	 */
	adrp	x0, swapper_pg_dir
	mov_q	x5, KIMAGE_VADDR + TEXT_OFFSET	// compile time __va(_text)
	add	x5, x5, x23			// add KASLR displacement
	create_pgd_entry x0, x5, x3, x6
	adrp	x6, _end			// runtime __pa(_end)
	adrp	x3, _text			// runtime __pa(_text)
	sub	x6, x6, x3			// _end - _text
	add	x6, x6, x5			// runtime __va(_end)
	create_block_map x0, x7, x3, x5, x6


idmap 영역은 idmap_text_start ~ idmap_text_end 영역으로 선언 되어있는 반면 kernel image 영역은 swapper_pg_dir로 되어있다. kernel image의 앞부분을 swapper 영역이라고 하는건가보다. 그런데 뭐에 쓰는 녀석인지는 아직 잘.... 뭐지?


6. __cpu_setup


함수 콜은 head.S에 있지만 구현은 mm/proc.S 에 있다. 이곳에선 kernel 시작 하기 전에 CPU의 system register값을 설정해준다.


ENTRY(__cpu_setup)
	tlbi	vmalle1				// Invalidate local TLB
	dsb	nsh

	mov	x0, #3 << 20
	msr	cpacr_el1, x0			// Enable FP/ASIMD
	mov	x0, #1 << 12			// Reset mdscr_el1 and disable
	msr	mdscr_el1, x0			// access to the DCC from EL0
	isb					// Unmask debug exceptions now,
	enable_dbg				// since this is per-cpu
	reset_pmuserenr_el0 x0			// Disable PMU access from EL0

	ldr	x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
		     MAIR(0x04, MT_DEVICE_nGnRE) | \
		     MAIR(0x0c, MT_DEVICE_GRE) | \
		     MAIR(0x44, MT_NORMAL_NC) | \
		     MAIR(0xff, MT_NORMAL) | \
		     MAIR(0xbb, MT_NORMAL_WT)
	msr	mair_el1, x5

	adr	x5, crval
	ldp	w5, w6, [x5]
	mrs	x0, sctlr_el1
	bic	x0, x0, x5			// clear bits
	orr	x0, x0, x6			// set bits

	ldr	x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
			TCR_TG_FLAGS | TCR_ASID16 | TCR_TBI0
	tcr_set_idmap_t0sz	x10, x9

	mrs	x9, ID_AA64MMFR0_EL1
	bfi	x10, x9, #32, #3
#ifdef CONFIG_ARM64_HW_AFDBM
	mrs	x9, ID_AA64MMFR1_EL1
	and	x9, x9, #0xf
	cbz	x9, 2f
	cmp	x9, #2
	b.lt	1f
	orr	x10, x10, #TCR_HD		// hardware Dirty flag update
1:	orr	x10, x10, #TCR_HA		// hardware Access flag update
2:
#endif	/* CONFIG_ARM64_HW_AFDBM */
	msr	tcr_el1, x10
	ret					// return to head.S
ENDPROC(__cpu_setup)


코드가 길어서 주석은 다 뺏다. 중요하다고 여겨지는 부분들만 체크해보자.


cpacr_el1의 값을 써주는 작업은 해당 CPU에 FP(Floating Point), ASIMD(Advanced Single Instruction Multiple Data) 작업을 허용하겠다는 작업이다. 컴퓨터 아키턱처 시간에 배운 용어들이 새록새록 떠오른다. FP, ASIMD에 대한 세부 구현은 CPU 제조사들이 하는 거고 개발자들은 그냥 기능만 켜주면 되니까 편리하다.


sctlr_el1은 System Control Register다. 여기선 값을 다시 써주기 보단 값을 읽고 필요한 정보를 가져오는 작업만 하고 있다.


ID_AA64MMFRx_EL1는 현재 하드웨어의 메모리 모델 및 정보를 확인할 수 있는 플래그다. 값을 새로 입력하는 건 불가능하고 값을 읽어오는 것만 가능하다. 위 정보를 통해 PARange bits(커버 할 수 있는 최대 메모리의 크기)와 Access bit, Dirty bit를 Hardware로 업데이트 할 수 있는지에 대한 플래그 값을 읽어온다.


요약하면 커널 내부에서 동작 할 수 있도록 cpu의 system register 값을 입력하는 작업을 한다.


7. __primary_switched


head.S 의 마지막 작업. kernel 함수로 점프하기 전에 필요한 레지스터 값을 복구하는 일을 한다. 마지막 줄에 b start_kernel 작업으로 kernel 함수를 실행하게 된다.


최근에 ARM 64bit 리눅스의 초기 부팅 어셈블리 코드(arch/arm64/kernel/head.S)를 분석할 일이 있었다. 학교 다닐 때 x86 어셈블리로 코딩을 해본적도 있고 예전에 ARM 32bit miniOS 초기 부분도 분석해본 경험이 있어서 금방 할 줄 알았는데... ldr 같은 기본적인 명령어도 오랜만에 보니 생소했고 ARM 64bit만의 고유한 레지스터가 있어 레퍼런스를 뒤적거리면서 찾게 되다 보니 생각보다 오랜 시간이 소요됐다. ARM32와 ARM64가 원래 겹치는 영역이 별로 없는건지 아니면 내 머릿속에 남아 있는게 별로 없어서 겹칠게 없어진건지.


고생한 만큼 쉽게 잊혀질 수 있기 때문에(응?) 포스트로 이번에 공부한 내용들을 짧게나마 정리해보려한다.


1. ENTRY(stext)


 
ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	__create_page_tables
	/*
	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
	 * details.
	 * On return, the CPU will be ready for the MMU to be turned on and
	 * the TCR will have been set.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)


stext에 있는 영역은 커널 바이너리 이미지가 압축 해제된 후 PC값이 첫번째로 가리키는 영역이다. 아키텍처와 무관한 리눅스 코드를 실행하기 전에 ARM에서 필수적으로 해야하는 작업을 여기서 처리한다. 브랜치의 이름만으로도 대략 무슨일을 하는지 짐작 할 수 있다.


2. preserve_boot_args


preserve_boot_args:
	mov	x21, x0				// x21=FDT

	adr_l	x0, boot_args			// record the contents of
	stp	x21, x1, [x0]			// x0 .. x3 at kernel entry
	stp	x2, x3, [x0, #16]

	dmb	sy				// needed before dc ivac with
						// MMU off

	mov	x1, #0x20			// 4 x 8 bytes
	b	__inval_dcache_area		// tail call


Bootloader에서 kernel을 올려주면서 x0~x3에는 특정한 argument를 전달한다. 그런데 x0~x3 레지스터는 연산 할 때 자주 사용되는 레지스터이므로 값이 변경될 소지가 있다. 그래서 넘어 온 값들은 boot_args의 주소 영역에다가 값을 저장하도록 한다. 나중에 common 폴더에 있는 리눅스 초기 작업을 실행하기 전에 복구해야한다.


3. el2_setup


만약 리눅스가 EL2에서 시작이 됐다면 기존에 EL1에서 돌아가도록 짠 리눅스 코드가 실행되는데 문제가 없도록 미리 몇몇 작업을 처리할 필요가 있다. 구체적으로 무슨일을 하는지는 코드를 차근차근 따라가보자.


ENTRY(el2_setup)
	msr	SPsel, #1			// We want to use SP_EL{1,2}
	mrs	x0, CurrentEL
	cmp	x0, #CurrentEL_EL2
	b.eq	1f
	mrs	x0, sctlr_el1
CPU_BE(	orr	x0, x0, #(3 << 24)	)	// Set the EE and E0E bits for EL1
CPU_LE(	bic	x0, x0, #(3 << 24)	)	// Clear the EE and E0E bits for EL1
	msr	sctlr_el1, x0
	mov	w0, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret

1:	mrs	x0, sctlr_el2
CPU_BE(	orr	x0, x0, #(1 << 25)	)	// Set the EE bit for EL2
CPU_LE(	bic	x0, x0, #(1 << 25)	)	// Clear the EE bit for EL2
	msr	sctlr_el2, x0

#ifdef CONFIG_ARM64_VHE
	/*
	 * Check for VHE being present. For the rest of the EL2 setup,
	 * x2 being non-zero indicates that we do have VHE, and that the
	 * kernel is intended to run at EL2.
	 */
	mrs	x2, id_aa64mmfr1_el1
	ubfx	x2, x2, #8, #4
#else
	mov	x2, xzr
#endif


mrs/msr 은 co-processor의 primary register 값을 읽고 쓰는데 사용하는 명령어다. 주로 p13,14,15 의 레지스터값을 읽는다. ARM 32bit 버전에서는 인자가 5개일 정도로 명령어가 복잡했는데 ARM64 들어오면서 각각에 특정 레지스터 값을 입력하는 것으로 바뀌었나보다. 그래선지 예전에 분석 할 때 보다는 쉬웠던 것 같다. 레지스터가 저장되는 방향은 꼭 기억하자. 오른쪽에 있는 레지스터 값이 왼쪽 레지스터에 저장된다!


상단부 ret 명령어 전까지는 리눅스가 어떤 모드에서 실행됐는지를 확인한다. EL2로 실행되면 1: 로 PC 값이 이동하고 그렇지 않으면 Endian bit를 세팅하고 넘어간다. 현재 EL1에서 실행되고 있는 것이 확실하므로 EL1에서 사용할 수 있는 System Control Register에 현재 바이너리 형식이 Little Endian인지 Big Endian인지 세팅한다.


하단부는 VHE 유무를 세팅한다. VHE인 경우는 뭔가를 좀더 많이 하는데... 현재 VHE를 사용 할 수 있는 보드는 이세상에 없으므로 패쓰. 더 밑으로 내려가보자


	/* Hyp configuration. */
	mov	x0, #HCR_RW			// 64-bit EL1
	cbz	x2, set_hcr
	orr	x0, x0, #HCR_TGE		// Enable Host Extensions
	orr	x0, x0, #HCR_E2H
set_hcr:
	msr	hcr_el2, x0
	isb

	cbnz	x2, 1f
	mrs	x0, cnthctl_el2
	orr	x0, x0, #3			// Enable EL1 physical timers
	msr	cnthctl_el2, x0
1:
	msr	cntvoff_el2, xzr		// Clear virtual offset

#ifdef CONFIG_ARM_GIC_V3
	/* GICv3 system register access */
	mrs	x0, id_aa64pfr0_el1
	ubfx	x0, x0, #24, #4
	cmp	x0, #1
	b.ne	3f

	mrs_s	x0, SYS_ICC_SRE_EL2
	orr	x0, x0, #ICC_SRE_EL2_SRE	// Set ICC_SRE_EL2.SRE==1
	orr	x0, x0, #ICC_SRE_EL2_ENABLE	// Set ICC_SRE_EL2.Enable==1
	msr_s	SYS_ICC_SRE_EL2, x0
	isb					// Make sure SRE is now set
	mrs_s	x0, SYS_ICC_SRE_EL2		// Read SRE back,
	tbz	x0, #0, 3f			// and check that it sticks
	msr_s	SYS_ICH_HCR_EL2, xzr		// Reset ICC_HCR_EL2 to defaults


Hyp configuration 이라고 되어 있는데.. 사실 앞에서 VHE를 사용하는게 아니라면 하이퍼바이저를 사용하기 위한 작업은 아니라고 봐도 될 것 같다. x0 레지스터에 현재 EL1은 64bit로 돌아가고 있다고 세팅한 후 VHE 모드가 켜져 잇는지 확인하고(x2는 앞서 세팅 했었다) set_hcr로 넘어간다. 그리고 hcr_el2, Hypervisor Configuration Register의 값을 세팅한다. 그리고 그 아래엔 cnthctl_el2 레지스터의 값을 바꿔서 Non-secure EL1에서 physical counter, timer를 읽을 때 EL2로 trap이 되지 않도록 한다. 리눅스는 EL1으로 돌아가야 하니까 그런듯 하다. 그 아래 CONFIG_ARM_GIC_V3 지시어가 있는 부분도 동일하다. EL2로 트랩되지 않도록 처리하고 넘어가버린다. ICC_SRE_EL2는 GIC에 있는 레지스터다. 


	
install_el2_stub:
	/*
	 * When VHE is not in use, early init of EL2 and EL1 needs to be
	 * done here.
	 * When VHE _is_ in use, EL1 will not be used in the host and
	 * requires no configuration, and all non-hyp-specific EL2 setup
	 * will be done via the _EL1 system register aliases in __cpu_setup.
	 */
	/* sctlr_el1 */
	mov	x0, #0x0800			// Set/clear RES{1,0} bits
CPU_BE(	movk	x0, #0x33d0, lsl #16	)	// Set EE and E0E on BE systems
CPU_LE(	movk	x0, #0x30d0, lsl #16	)	// Clear EE and E0E on LE systems
	msr	sctlr_el1, x0

	/* Coprocessor traps. */
	mov	x0, #0x33ff
	msr	cptr_el2, x0			// Disable copro. traps to EL2

	/* SVE register access */
	mrs	x1, id_aa64pfr0_el1
	ubfx	x1, x1, #ID_AA64PFR0_SVE_SHIFT, #4
	cbz	x1, 7f

	bic	x0, x0, #CPTR_EL2_TZ		// Also disable SVE traps
	msr	cptr_el2, x0			// Disable copro. traps to EL2
	isb
	mov	x1, #ZCR_ELx_LEN_MASK		// SVE: Enable full vector
	msr_s	SYS_ZCR_EL2, x1			// length for EL1.

	/* Hypervisor stub */
7:	adr_l	x0, __hyp_stub_vectors
	msr	vbar_el2, x0

	/* spsr */
	mov	x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\
		      PSR_MODE_EL1h)
	msr	spsr_el2, x0
	msr	elr_el2, lr
	mov	w0, #BOOT_CPU_MODE_EL2		// This CPU booted in EL2
	eret


install_el2_stub 로 오기 전에 debug 옵션을 켜주는 코드가 있는데 그 부분은 생략했다. 내가 분석한 코드랑은 조금 달랐다.. 버전상의 차이가 있는 건가. 아마 맥락은 비슷할 것이다. "PMU가 있는지 확인해보고 있으면 디버그를 키고 아니면 스킵한다." 이 정도만 확인하면 될 것 같다.


사실 요 브랜치에서 주의깊게 봐야하는 코드는 install_el2_stub 의 작업이다. ARM에서 가상화가 지원되면서 EL1에서 실행되는 리눅스가 초기화 작업중 EL2로 모드를 변경 할 때 어떻게 trap을 잡아낼 것인가가 주요 관심사였다. 당시 논문에서는 arm 초기화 부분에서 임시로 vector table을 두어 처리 한다고 했었는데 실제로도 vbar_el2(Vector base address register) 레지스터를 hyp_stub_vector로 세팅하도록 해서 처리하고 있다. vector table의 코드를 보면 부팅시에만 임시로 사용하는 테이블이어서 EL1에서 오는 trap 말고는 모두 invalid하게 처리해뒀다. 실제로 사용할 vector table을 세팅하기 전까지만 사용하는 코드라 그런것 같다.


그 아래는 spsr_el2, elr_e2 값을 세팅하는 작업이다. spsr은 Saved Program Status Register로 프로세스 실행중 Exception이 일어 날 때 현 프로세스의 상태(pstate)를 저장해둬 Exception 처리 후 원래 상태를 복구 할 때 사용하는 레지스터고 elr 레지스터는 exception 처리후 이동할 pc값을 저장하고 있다. 음 그런데 뭔가 이상하다. 지금 Exception이 일어날 상황이 아닌데 왜 spsr 값과 elr 값을 지정하는거지?


eret 명령어를 확인하면 왜 spsr, elr 값을 변경했는지 알 수 있다. eret는 exception 처리후 원래 상태로 돌아갈 때 사용하는 명령어다. eret가 실행되면 spsr값이 pstate 값으로 바뀌고 pc 값은 elr을 가리키게 된다. lr은 원래 el2_setup으로 넘어오기 전에 다음에 실행할 명령어를 세팅해뒀을 것이다. 즉 eret가 실행되면서 el2_setup 다음의 명령어로 이동하게 된다.


프로그램 상태는 spsr에 저장한 값으로 세팅한다. 오 그런데 spsr 값 마지막에 PSR_MODE_EL1h라고 세팅해뒀다. 즉 원래 프로그램이 EL1로 실행되고 있었다고 속이는 것이다. 이렇게 하면 eret가 실행되면 CPU는 EL1 모드로 변경돼 실행하게 된다. 즉 원래 EL1에서 실행됐던 리눅스의 루틴을 따르도록 모드를 변경하는 작업인 것이다. 대충 분석했으면 모드 변경 코드를 찾지 못했을 것 같은데. 이렇게 깜쪽같이 슬쩍 넣어놓다니. 원래 이런 방식으로 했던걸까?


정리하다보니 내용이 너무 길다. 나머지 내용은 다음 포스트에!

입출력제어(ioctl)

컴퓨터공부/리눅스 2017.02.11 13:10 Posted by 아는 개발자

리눅스는 크게 시스템 영역(kernel)과 사용자 영역(user space)을 분리해서 악의적으로 만든 사용자 애플리케이션이 시스템 핵심 영역에 침범 할 수 없도록 만들어졌다. 하지만 애플리케이션을 개발하다보면 커널 영역내에 있는 함수들을 사용해야 할 일이 있는데 이런 경우 커널에서는 기본적으로 시스템 콜을 이용해 커널영역에 있는 함수들을 사용 할 수 있도록 지원한다. 하지만 시스템 콜은 기껏 해야 300개 정도 등록 할 수 있는데 모든 사용자 애플리케이션이 시스템 콜에 필요한 함수를 등록하기엔 수가 부족하다.


이때 사용 할 만한 툴이 ioctl이다. ioctl은 유저영역에 있는 애플리케이션이 현재 동작 중인 드라이버에 값을 전달하거나 받아 올 수 있도록 한다. 동작 과정을 간단히 그림으로 설명해보면 다음과 같다.


User 영역에 있는 application은 현재 등록되어있는 "/dev/[device]"파일을 open하고 필요한 명령어들을 ioctl로 전달한다. 입력한 device파일을 등록한 모듈은 이 드라이버를 등록 했을 때 ioctl command에 맞춰 처리할 루틴을 설정해두면 User에서 받아온 argument로 값을 전달 받거나 또는 다시 전달 할 수 있다.


주로 개발한 드라이버에 사용자 애플리케이션 영역에서 접근 하고 싶을 때 사용한다. 프린터에 주로 사용하는 것 같다.

디바이스트리(Device Tree)

컴퓨터공부/리눅스 2017.01.04 19:07 Posted by 아는 개발자

운영체제(Operating System)가 하드웨어와 소프트웨어 사이의 중간자 역할을 하고 사용자의 애플리케이션이 하드웨어를 조작하기 쉬운 환경을 제공 한다는 것은 컴퓨터 전공자라면 운영체제 수업시간에 귀가 빠지도록 배웠을 것이다. 운영체제는 오드로이드, 아두이노, 주노보드처럼 다양한 보드 위에 있는 RAM, CPU, EMMC등 하드웨어들을 초기화 및 조작하는 역할을 한다. 지금 이 글을 쓰기 위해 타자를 치는 동안 운영체제는 타자 입력 인터럽트를 처리하고 소프트웨어에 전달하는 일을 계속 하고 있다.


어떤 메인 보드를 사용하더라도 운영체제의 기본 뼈대는 동일해야 한다. 하지만 메인 보드들마다 상세 하드웨어 스펙은 천지 차이다. 예를 들면 RAM의 크기나 USB의 물리 주소의 위치처럼 당연한 것 부터 이 인터럽트 핸들러는 몇번까지 등록하는지 등등 생각하면 한도 끝도 없다. 이런 하드웨어 정보들을 모두 운영체제 코드 내에 하드코딩 해버린다면... 아마 코드가 매우 지저분해지고 엉망이 될 것이다. 그렇다고 하드웨어 제조사들은 많고 또 저마다 자기들이 최고라고 자랑하는 보드를 내놓는데 안 쓰기는 아깝고. 


간단히 하드웨어 정보들만 정리해서 던져주면 운영체제가 전달받은 정보들을 활용해 하드웨어를 전달하면 어떨까? 하드코딩으로 잘 부분을 전달받은 정보들로만 치환해주면 될 것 같아 깔끔해보인다.


이런 구조를 리눅스 OS는 이미 구현 해뒀다. ARM기반의 보드를 사용하는 경우 리눅스를 부팅을 위해선 디바이스 정보들을 바이너리 형태로 싹 저장해둔 디바이스 트리라는 것이 필요하다. 부팅시 리눅스는 디바이스 트리 정보들을 쭉 훑어보고 하드웨어 초기화시 필요한 정보들을 파싱(parsing)해서 사용한다. 예를들어 빈 메모리 영역을 관리할 Buddy system을 만들고 싶다면 먼저 전체 메모리의 크기는 얼마인지도 알아야 하고 물리주소 몇 번부터 몇 번까지를 메모리 영역으로 사용 할 수 있는지도 알아야 한다. 디바이스 트리에 이런 정보들을 모두 담아 뒀으니 리눅스는 그냥 여기 있는 정보들을 가져다가 사용하면 된다.



Device Tree안의 메모리 정보가 있는 곳에서(노드라고 부른다) reg 값의 앞에 두 셀을 Address, 뒤의 두 셀을 Size로 정해서 시작 주소와 하드웨어의 사이즈를 읽어 올 수 있다.


운영체제가 사용하는 디바이스 트리 정보들은 모두 바이너리로 되어 있다. 이런 하드웨어 정보들은 제조사들이 dts파일인 스크립트 형태로 만들어 두고 커널 빌드시 디바이스트리도 같이 빌드돼어 dtb 형태의 바이너리 파일이 만들어진다. 리눅스 커널을 받아보면 arch/arm64/boot/dts 폴더 안에 보드에 따라 설정된 device tree 스크립트를 확인 할 수 있다. 



디바이스 트리 문법을 알고 싶다면 https://www.devicetree.org/ 요 사이트를 한번 참고해보길 바란다. 예전에 문법공부한다고 참고하던 사이트가 있었는데 어디갔는지 모르겠다. 어렵지 않으니 그냥 스크립트만 봐도 금방 따라 갈 수 있을 것이다.


만든 스크립트는 dtc라는 컴파일러를 이용해 바이너리로 만들 수 있다. 우분투에 패키지 형태로 제공되므로 다운받아서 사용하면 편하다.


x86에서는 ACPI를 사용한다.

커널은 크게 커널 내에 넣는 feature의 양에 따라 모놀리식(Monolithic) 커널과 마이크로(Micro) 커널로 나눌 수 있다.



위 구조를 이해 할 때 통념을 좀 깨야 할 필요가 있었다. 

나는 너무도 당연히 kernel이 device driver나 File system을 관리 한다고 생각했는데 꼭 그런 것 만은 아니었다.


모놀리식 커널은 리눅스로 OS를 배운 학부생에게 가장 익숙한 OS이다. Application을 제외한 모든 system 관련 기능들(VFS, IPC, Filesystem 등등)을 커널이 관리하며 각 영역들은 단계적으로 나뉘어 있다. 생각해보면 리눅스 프로세스 하나를 만들 때 VFS에서 제공하는 fopen 라이브러리를 이용해 다른 파일 시스템에 쉽게 접근이 가능했고, IPC를 이용해 다른 프로세스에게 메시지를 전달 하는 것도 척척 해낼 수 있었는데 위 모든것은 커널의 system call을 이용해 했던 방식 들이었다. 커널이 모든 시스템 서비스들을 관리 했기 때문에 개발자는 커널이 제공하는 함수만 사용해서 쉽게 할 수 있었다. * 대표적인 예로는 Unix, Linux가 있다.


하지만 마이크로 커널은 마이크로라는 접두어에서도 의미하듯 핵심적인 기능(스케줄링, 메모리 관리 등등)만 커널에 담고 나머지는 제외해 가볍게 만든 커널이다. 기존에 모놀리식 커널이 갖고 있던 시스템 기능들(VFS, IPC, Device driver)은 커널위의 서버의 형태로 존재한다(위 개념을 받아들이는게 좀 충격이었다). 이러한 방식이 장점의 장점은 하나의 서비스가 죽더라도 커널 전체가 panic되지는 않다는 점이다. 예를들어 Device Driver 하나가 죽더라도 전체 커널이 죽는 일은 없다. 리얼 타임성이 중요한 임베디드 시스템에서 주로 사용된다. * 대표적으로 MacOS X, Windows NT가 있다.


(마이크로에서 특이한 점은 File system이나 Driver가 유저 스페이스에 들어간다는 점!)



각 구조마다 장 단점이 있다.


모놀리식 커널

  • 장점 : 각 Component간의 커뮤니케이션이 효율적이다.
  • 단점 : 디바이스 드라이버를 추가/삭제 하려면 커널을 재빌드 해야 한다. 또한 하나가 죽으면 전체 시스템이 죽는다.
마이크로 커널
  • 장점 : 서버를 추가하는 방식이기 때문에 기능을 추가하기 쉽고, 시스템이 견고하며 리얼타임성이 높다
  • 단점 : 시스템 기능들이 서버의 형태로 존재하기 때문에 커뮤니케이션 오버헤드가 있다.

위 두가지를 짬뽕한 구조가 모듈형 커널이라 한다. 쉽게 모듈의 형태로 쉽게 시스템 기능을 추가 할 수 있는 방식인데 윈도우가 가장 대표적이다. 다음에는 모듈형 커널에 대해서 공부해봐야겠다.

  1. 동서방불패 2017.08.02 14:41  댓글주소  수정/삭제  댓글쓰기

    좋은 정보 감사합니다.

init 프로세스 그리고 systemd

컴퓨터공부/리눅스 2016.10.08 16:31 Posted by 아는 개발자

리눅스 pc를 부팅 할 때 커널 메모리에 로드되고 여러가지 초기화 과정(하드웨어, 페이지 테이블)이 정상적으로 이뤄지면 커널은 프로세스들을 만들어 사용자가 컴퓨터가 사용 할 수 있는 환경을 만드는데 이때 가장 먼저 만들어 지는 프로세스가 init 프로세스이다


(첫번째로 만들어지기 때문에 pid를 1을 가진다. 프로세스 이름은 init이다)


init프로세스는 가장 먼저 생성되어 부팅시 자동으로 실행해놓은 프로그램들(네트워크 서비스나 usb driver 등록 등등)을 실행시켜준다. 실행시켜주는 방법은 간단히 fork 해서 프로세스를 생성하는 것이다. 정상 부팅후 pstree로 프로세스간 관계를 살펴보면 다음과 같다.


(init 프로세스를 시작으로 여러 프로세스들이 얽혀서 생성되었다. 모든 프로세스들의 조상인 느낌이다, 하지만 가장 마지막에 죽는다)


밖에 보이지 않고 뒤에 숨어서 여러 프로세스를 관리하는 것을 데몬이라고 하는데, init 프로세스가 가장 대표적인 예이다. 시스템이 꺼지기 전까지 살아남아서 시스템의 전반적인 사항들을 관리한다.


init 프로세스가 생성하는 프로세스의 목록은 /etc/rc[숫자].d 에 저장되어있다. 여기서 [숫자]는 부팅의 런레벨을 말한다. 런레벨이랑 리눅스 부팅의 종류를 말한다. 이렇게 간단히 UI가 뜨는 방식의 부팅이 있지만 싱글모드, 콘솔모드, 무한 재부팅모드 등 여러 모드가 존재한다. 우분투는 기본적으로 runlevel 2 로 부팅된다. runlevel 명령어로 현재의 위치를 확인 할 수 있고 /etc/init/rc-sysinit.conf를 통해 기본 runlevel을 바꿀 수 있다.



/etc/rc[숫자].d 폴더 내에는 부팅 runlevel에 해당하는 자동 실행 프로세스들을 갖고 있다. 폴더 내에 있는 파일들은 링크(바로가기)의 형태로 존재한다. 


(실제 바이너리는 /init.d/ 내에 모두 존재하는 것을 확인 할 수 있다)


프로세스 생성 작업들은 모두 일련화(Sequalization), 하나하나씩 차례대로 일어난다. "하나만 잘하자!"는 리눅스의 철학을 반영한 예라고 어떤 개발자는 그런다.


그런데 이것을 뒤집은 새로운 방식의 부팅 프로세스가 systemd이다.


init프로세스가 모든 작업을 차례차례 진행했다면, systemd는 프로세스 생성과정에서 병렬화 기능을 제공해(aggressive parallelization capabilites) init 프로세스보다 빠르게 부팅이 가능하도록 한다. 더 많은 feature들이 있는 것 같은데 아직 전반적인 지식이 부족해서 이해하지는 못하고 있다..


아직 모든 리눅스 OS에 적용된것은 아니고, Fedora, CentOS7 에만 적용되었다. Debian에서는 아직 정식으로 적용되진 않았다.

Linux OS 부팅 과정

컴퓨터공부/리눅스 2016.10.02 17:18 Posted by 아는 개발자

이번 글을 리눅스 OS가 설치된 컴퓨터가 부팅 되는 과정을 설명하는 포스트다. 부트로더가 하는 역할과 초기 부팅시 커널 이미지가 어떤 작업을 하는지 파악하는것에 초점을 두고 작성했으며 중간중간 이들의 정의도 포함되있다.


http://www.tldp.org/HOWTO/HighQuality-Apps-HOWTO/boot.html 페이지를 참고해서 글을 작성했다.


1. 컴퓨터 전원 On


사용자가 컴퓨터 전원을 키면 메인보드에서는 전원이 켜진것을 확인하고 특정 저장위치에 심어둔 부트로더를 실행시킨다.


2. 부트로더(Boot loader)


부트로더의 기능은 말그래도 부팅할 때(Boot) 로드(Load)하는 역할을 한다. 무엇을 로드하는지가 중요한데 OS에 따라 다르지만 리눅스를 기준으로 설명하면, 부트로더는 리눅스가 구동할 커널 이미지(zImage)와 부팅 디바이스의 정보(device tree block)를 로드하는 역할을 한다. 커널이미지와 device tree block은 디스크 저장소에 존재한다. 사용자가 부트로더 어떤 부분을 읽어오라 설정을 하면 바이오스는 해당 위치에 존재하는 바이너리 파일(이미지)를 읽어오도록 구동된다. 여기서 어떤 하드디스크를 사용하느냐에 따라 로딩 속도가 달라진다. SDD가 HDD보다 부팅속도가 빠른것도 위와 같은 이유 때문이다.




로드 하는 곳은 특정 램 영역이다. 커널 이미지는 컴퓨터가 꺼지기 전까지 항상 램 영역에 상주해 컴퓨터 작동에 필요한 코드를 제공한다. 커널 이미지가 위치하는 램 영역 또한 부트로더를 이용해 설정이 가능하다.


대표적인 부트로더로는 바이오스와 u-boot가 있는데 사용하는 Architecture로 구분된다. x86은 주로 바이오스를 사용하고 ARM은 u-boot를 사용한다. 그런데 요새는 둘다를 통합 할 수 있는 UEFI를 사용하는 추세다.


3. 커널 이미지 코드 실행


부트로더 작업이 모두 끝나면 부트로더는 CPU가 다음에 실행하는 코드 (pc값)을 커널 내부로 넘겨서 제어권을 커널에게 줘버린다. 그러면 이제부턴 온전히 커널의 독무대이다. 


( Program Counter 값을 커널 이미지가 위치한 곳으로 옮겨서 앞으로 커널 코드를 수행하도록 한다 )


커널은 개발자가 작성해둔 작업들을 실행한다. 그 작업들로는


      • 페이지 테이블 초기화 작업
      • 하드웨어 초기화 작업(블루투스, 마우스, 키보드 등등...)
      • 드라이버 초기화 작업
      • 네트워크 접속
      • Secondary core 초기화 작업
      • 등등.. 무수히 많다...
OS를 실행 할 때 필요한 초기화 작업들을 모두 한다. 맨처음 부팅할 때 나오는 커널 메시지들이 요런 녀석들이다. 컴퓨터 킬 때 스크린에서 한 번 쯤 봤을 로그들이다.


[    0.191970] ... value mask:             0000ffffffffffff

[    0.191971] ... max period:             000000007fffffff

[    0.191972] ... fixed-purpose events:   0

[    0.191972] ... event mask:             000000000000000f

[    0.193308] x86: Booted up 1 node, 1 CPUs

[    0.193310] smpboot: Total of 1 processors activated (5184.01 BogoMIPS)

[    0.193488] NMI watchdog: disabled (cpu0): hardware events not enabled

[    0.193489] NMI watchdog: Shutting down hard lockup detector on all cpus

[    0.193643] devtmpfs: initialized

[    0.193898] evm: security.selinux

[    0.193899] evm: security.SMACK64

[    0.193900] evm: security.SMACK64EXEC

[    0.193900] evm: security.SMACK64TRANSMUTE

[    0.193901] evm: security.SMACK64MMAP

[    0.193902] evm: security.ima

[    0.193902] evm: security.capability 


맨 처음 시작 할 때의 코드는 head.S 이다. 코드는 요기에 있다. 첫 실행 파일은 어셈블리 언어로 이뤄져 있다. 부팅에 필요한 기본적인 작업들만 수행하고나서 커널로 점프를 한다.


linux/arch/arm/boot/compressed/head.S -> ARM버전

linux/arch/x86/boot/compressed/head_32.S -> x86 32bit 버전


부팅이 정상적으로 마무리 되면, 커널은 루트파일 시스템을 mount하는 작업을 한다.


4. 루트파일 시스템에 mount 작업


마운트 하는 작업을 설명하기 전에 먼저 루트파일 시스템에 대해서 설명부터 하자. 리눅스 커널을 사용하는 OS로는 CentOS, 우분투, 레드햇, Fedora 등등 여러가지가 있다. 그러나 동일하게 리눅스 커널을 사용하기는 하는데 막상 부팅하고 보면 로그인 화면은 모두 차이가 난다. 사용하다보면 폰트도 다르고, 루트 파일의 폴더명도 다르며 패키지 관리 방식과 네트워크 설정 화면 등등 모든것이 다르다. 이것들은 모두 루트파일 시스템이 달라서 그런 것이다. 동일한 리눅스 커널을 사용한다는 것은 최하윗단에서 하드웨어의 리소스를 관리를 리눅스 코드를 사용한다는 것이지 이들의 GUI가 동일하다는 것은 아니다. 커널 소스를 제외한 껍데기(GUI, 기본 명령어, 폴더명 등등...)를 말하는 것이 루트파일 시스템이다. 


커널은 부팅과정에서 자신이 할 일을 마치면 개발자가 미리 설정해둔 루트 파일 시스템 파티션에 mount작업을 실시하고 루트 파일 시스템 실행시 필요한 작업들을 실행한다. OS 버전마다 초기에 시작하는 프로그램이 다르고 또 이들을 실행하는 방식이 다르다. 크게 init.d, systemd로 나뉜다. init.d로 설명하면 pid 1번으로 등록되는 init process가 부팅에 필요한 프로그램들을 실행시킨다.


( 루트파일 시스템 실행중 나오는 로그 )


정상적으로 마운트 되고나면 각 OS마다 제공하는 로그인화면이 뜨게 된다. 모든 부팅이 종료된 상황이다.