Writings/Linux kernel2017. 12. 11. 05:22

다음으로 소개할 방법은 cgroup을 이용하는 방법이다.

Cgroup (control group)은 리눅스 커널이 제공하는 시스템의 자원 사용률을 그룹별로 제어하기 위한 방법이다.

이 또한 sysfs 인터페이스를 통해 사용 가능한데, 기존 cgroup v1과 Linux 4.5.x 때부터(정확한지는 모르겠지만 이때쯤) 지원하기 시작한 cgroup v2 두 종류가 있다. 사용방법이 사뭇 다르므로 정확한 용법은 커널 도큐먼트(Documentation/cgroup-v2.txt)를 확인해 보는 것이 좋다. 

일단 아무데나 원하는 위치에 디렉토리를 만들고, 다음과 같이 cgroup sysfs를 마운트하자.


# mkdir cgroup

# mount -t cgroup2 none ./cgroup


해당 마운트포인트에서 ls -l 명령을 실행시키면 다음과 같은 결과가 나올 것이다.


-r--r--r-- 1 root root 0 Dec  9 03:20 cgroup.controllers

-rw-r--r-- 1 root root 0 Dec  9 03:20 cgroup.procs

-rw-r--r-- 1 root root 0 Dec  9 03:21 cgroup.subtree_control


여기서 cgroup.controllers는 현재 cgroup v2가 사용가능한 컨트롤러들을 뜻한다. 부트 파라미터를 통해 cgroup v1을 효과적으로 disable 했다면, (혹은 애초에 cgroup v1을 사용하지 않는다면) 다음과 같은 결과를 볼 수 있다.


# cat cgroup.controllers

io memory pids


I/O와 memory, 그리고 pid에 대한 컨트롤러를 제공하며, 이 말은 cgroup 인터페이스를 통해 원하는 그룹에 저 세 가지 자원에 대한 제한을 걸 수 있다는 뜻이 된다.

cgroup.procs 는 현재 해당 컨트롤 그룹에 속해있는 프로세스를 의미하며, 현재 시스템에 등록되어 동작하고 있는 대부분의 프로세스가 여기에 등록되어 있을 것이다. 즉, 마운트포인트의 루트 디렉토리는 시스템의 루트 컨트롤 그룹에 대한 정보가 등록되어 있는 것이다. 

cgroups.subtree_control 은 하위 컨트롤 그룹에 대해 어떤 컨트롤러를 사용할 것인지 지정하는 것이다. 현재는 아무런 정보도 없을 것이다.


이제 새로운 컨트롤 그룹을 생성할 시간이다. 아래의 명령을 입력해보자.


# mkdir cgroup_child


생성된 디렉토리 아래로 가게 되면, 부모 디렉토리와 마찬가지로 cgroup.controllers, cgroup.subtree_control, cgroup.procs, 그리고 cgroup.events 파일이 존재할 것이다. 

생성된 그룹에 프로세스를 추가하기 위해서는, 해당 프로세스의 pid를 알아내어 다음과 같이 입력하면 된다.


# echo "pid" > ./cgroup/cgroup_child/cgroup.procs


그리고 원하는 컨트롤러를 "부모" 디렉토리의 cgroup.subtree_control에 입력하자. 여기서는 페이지 캐시 사용량을 제한하려고 하니, memory 컨트롤러를 입력하면 된다.


# echo "+memory" > ./cgroup/cgroup.subtree_control


여기서 +는 해당 컨트롤러를 추가한다는 의미이며, -로 입력할 경우 해당 컨트롤러를 제거한다는 의미가 된다. 

해당 명령을 수행하면, 자녀 디렉토리에 다음과 같은 파일들이 추가로 생성된다.


-r--r--r-- 1 root root 0 Dec  9 03:21 cgroup.controllers

-r--r--r-- 1 root root 0 Dec  9 03:21 cgroup.events

-rw-r--r-- 1 root root 0 Dec  9 03:21 cgroup.procs

-rw-r--r-- 1 root root 0 Dec  9 03:21 cgroup.subtree_control

-r--r--r-- 1 root root 0 Dec  9 03:21 memory.current

-r--r--r-- 1 root root 0 Dec  9 03:22 memory.events

-rw-r--r-- 1 root root 0 Dec  9 03:21 memory.high

-rw-r--r-- 1 root root 0 Dec  9 03:22 memory.low

-rw-r--r-- 1 root root 0 Dec  9 03:21 memory.max

-r--r--r-- 1 root root 0 Dec  9 03:21 memory.stat

각각의 의미는 다음과 같다.


 File 

Description 

 memory.current

컨트롤 그룹의 현재 메모리 사용량 

 memory.high

메모리 사용량의 soft limit 

 memory.low

메모리 사용량의 하한 

 memory.max

메모리 사용량의 hard limit 

 memory.events 

low, high, max, oom 에 대한 이벤트 횟수

 memory.stat

컨트롤 그룹의 메모리 사용량 통계 


메모리 사용량이 soft limit(memory.high)에 도달할 경우, 해당 그룹의 프로세스들을 throttling 하며, memory 회수를 빈번하게 수행시키기 위한 압박을 주게 된다. 만약, 메모리 사용량이 hard limit (memory.max)를 넘어서게 될 경우, 커널은 OOM killer를 호출하여 해당 프로세스를 종료시키게 된다.

두 항목에 적절한 값을 설정하고, free 명령을 통해 메모리 사용량 변화의 추이를 살펴보자.

root@ubuntu:/home/ubuntu# free -c 10

              total        used        free      shared  buff/cache   available

Mem:       65722960      410052    59913708        9736     5399200    64511200

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410612    59913580        9736     5398768    64510436

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410908    59913108        9736     5398944    64510268

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      411000    59913268        9736     5398692    64510304

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410824    59913176        9736     5398960    64510368

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410348    59913612        9736     5399000    64510896

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410980    59913340        9736     5398640    64509924

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410580    59913672        9736     5398708    64510560

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      411080    59912944        9736     5398936    64509680

Swap:      33326076           0    33326076


              total        used        free      shared  buff/cache   available

Mem:       65722960      410504    59913820        9736     5398636    64510612

Swap:      33326076           0    33326076


모든 가용영역을 캐시용도로 쓰던 과거에서 벗어나, 제한된 만큼 사용하는 것을 확인할 수 있다.


엄밀히 따지자면, 이 방법은 페이지 캐시를 제한하기 위해 존재하는 방법은 아니다.

하지만 내 용도가 페이지 캐시를 제한하는 것 이었던 만큼 이 항목으로 포스팅을 남긴다.

'Writings > Linux kernel' 카테고리의 다른 글

페이지 캐시의 크기 제한걸기 (1/2)  (0) 2017.12.10
Enabling Cgroup v2  (0) 2017.12.09
Posted by 곰푼
Writings/Operating System2017. 10. 18. 15:31

클락업을 통한 싱글 코어의 발전이 power wall에 가로막힌 이후, CPU의 발전 방향은 코어의 갯수를 늘리는 쪽으로 선회하였다. 그에 따라 멀티코어, 매니코어에 이르는 다양한 변종들이 등장하게 되었으며, 이로 인해 애플리케이션 및 운영체제의 확장성(scalability)이 중요한 문제로 대두되게 되었다. 


확장성이란 사전적인 의미로 보았을 때 늘어난 수요에 맞춰 얼마나 유연하게 대응할 수 있는지를 나타내는 척도라고 할 수 있다. 애플리케이션을 뒷 받침 하는 시스템의 입장에서 확장성이란 단어를 협의적으로 해석하면 늘어난 자원에 맞추어(e.g., 코어 수) 성능이 얼마나 향상되는 지를 나타내는 척도라고 할 수 있다. 자원이 늘어남에 따라서 성능이 비례하여 향상되면 확장성이 좋다 할 수 있고, 성능이 그대로이거나 혹은 더 떨어지게 된다면 확장성이 나쁘다라고 할 수 있다.


이 포스팅에서는 운영체제를 주로 다룰 것이므로 운영체제와 관련된 문제만을 살펴보도록 할 것이다. 확장성을 저해하는 요소는 운영체제 내에 다양하게 산재해 있지만, 그 중에서도 주된 요인을 꼽아보자면 단연 동기화(synchronization) 메커니즘이라고 할 수 있다.


비단 멀티코어 환경이 아니더라도 CPU의 시분할을 통한 멀티 프로그램 기법으로 인해 하나의 공유 자원에 대해 여러 프로세스가 동시에 접근할 수 있게 되었다. 각 프로세스는 특정 코드를 실행시키는 동안 자신이 접근하는 자원이 최소한 그 코드 내에서는 독점적으로 사용할 것을 요구하며, 이 요구사항이 지켜지지 않을 경우 일관성(consistency) 문제가 발생하게 된다. 


다음의 예시를 보자.


int a = 0, b = 0;

void func (void) {
    b = a + 1;  
    a = b; 
}

void proc_a (void) {
    func();
}

void proc_b (void) {
    func();
}


8번째 줄의 proc_a와 12번째 줄의 proc_b가 각각 별개의 프로세스에서 실행되는 코드라고 가정하자. proc_aproc_b가 동시에 실행된 뒤 a와 b를 출력하면 어떤 결과가 나올까? 총 두 가지의 결과를 예측해 볼 수 있다. 첫째로는 (a = 2, b = 2), 둘째로는 (a = 1, b = 1).  func()이 진행 중일 때 타이머 인터럽트가 발생하여 5번째 줄을 실행하기 전에 컨텍스트 스위치가 발생할 경우 두 번째의 결과가 발생하게 된다. 이러한 결과를 얻어내기 까지 발생할 수 있는 절차의 가지 수는 더욱 많다. 


  • proc_a -> proc_b
  • proc_b -> proc_a
  • proc_a -> (scheduling) proc_b -> proc_a
  • proc_b -> (scheduling) proc_a -> proc_b


위와 같은 상황이 발생할 경우, 사용자 혹은 애플리케이션의 입장에서는 같은 코드를 실행시켰음에도 불구하고 나오는 결과가 매번 달라질 수 있게된다. 따라서, 운영체제에서는 위와 같은 상황을 방지하기 위해 동기화 메커니즘을 제공한다. 대부분의 경우 동기화 메커니즘은 특정 영역(이를 critical section이라 부른다)에 대한 접근을 직렬화(serialize)하는 것으로 이러한 문제를 해결한다. 


void proc_a (void) {
    synch_region {
        func();
    }
}

void proc_b (void) {
    sync_region {
        func();
    }
}

<caller-side synchronization>

직렬화란, 동시에 오직 한 프로세스만 특정 영역에 접근시키도록 하는 것을 의미한다. 동시에 여러 프로세스가 접근할 경우, 뒤늦게 접근하는 프로세스는 앞서 진입한 프로세스가 작업을 마칠 때 까지 기다려야만 한다. 코드에서 synch_region으로 표시된 영역이 바로 이러한 영역이 되겠다.

위와 같이 호출하는 코드에서 동기화 영역을 표시하는 경우도 있으며, 반대로 호출되는 코드에서 동기화를 시키는 경우도 있을 것이다. 

void func (void) {
    synch_region {
        b = a + 1;  
        a = b; 
    }
}

<callee-side synchronization>

코드에 표시된 synch_region은 동기화 메커니즘의 일종의 예시인 것을 기억하자. 여러 논문 및 구현들을 통하여 다양한 방식의 동기화 메커니즘이 제안되어 왔으며, 위와 비슷한 형태를 취할 수도, 혹은 다른 형태를 취하게 될 수도 있다.


운영체제는 일반적으로 여러 형태의 locking primitive들을 통하여 동기화 기능을 제공한다. 대표적으로 spinlock과 같은 busy-loop 기반의 locking primitive가 있으며, 이는 효과적인 동기화 방법을 제공하지만 확장성을 저해시킬 수도 있다. 예를 들어 여러 코어에서 동작하는 프로세스들이 임계 영역(critical section - 동기화 메커니즘에 의해 보호받는 영역)을 만날 경우, 앞서 진입한 프로세스가 필요한 작업을 다 수행할 때 까지 기다려야만 한다. 이 임계 영역이 길어질 경우, 그만큼 대기 시간이 길어지게 되므로 자연스럽게 대기중인 프로세스가 점유하고 있는 코어의 시간을 낭비하게 된다. 


따라서 spinlock과 같은 busy-loop 기반의 동기화 방식들은 임계 영역의 길이를 최대한 줄일 것을 전제로 하고 있으며, 여의치 않을 경우 mutexsemaphore등의 block-based locking primitive들을 쓸 것을 추천한다. 이러한 lock들은 기다리는 프로세스를 sleep 상태로 전환함으로써, 해당 프로세스가 점유하고 있는 컴퓨팅 자원을 다른 프로세스에게 양도할 수 있도록 한다. 이를 통해 시스템은 낭비되는 컴퓨팅 자원을 효율적으로 활용할 수 있게 된다.


위와 같은 방식 외에도, 보다 확장성을 고려한 형태의 locking primitive들도 많이 고안되었다. reader-writer lock, ticket lock, queue spinlock, mcs lock, seqlock 등이 바로 확장성을 고려한 locking primitive 들이다.


앞으로 이와 같은 locking primitive들에 대해 한번 알아보려고 한다.

Posted by 곰푼
Tips/Kernel development2012. 9. 30. 01:21

Virtual File System


리눅스를 비롯한 대부분의 유닉스 계열 운영체제는 VFS라는 레이어를 제공한다. 이것은 표준의 SCI(System Call Interface)와 ext2, ext3 를 비롯한 실제 파일시스템 구현 사이에 존재하는 일종의 인터페이스이며, 커널은 이 인터페이스를 통해서 파일시스템 수준에서 동작해야 하는 작업을 처리하기 때문에 가상 파일 시스템(VFS)라는 이름이 붙게 되었다.


그렇다면 왜 VFS가 존재하게 되었는가?


컴퓨터 기술의 발전과 동시에 이를 관리하기 위한 시스템 기술들도 발전하게 되었다. 커널은 비선점형 커널에서 선점형 커널로 발전하였고, 멀티 코어를 지원하기 위한 개념들도 등장하게 되었다. 이는 저장장치와 이 저장장치를 다루는 드라이버 레벨의 소프트웨어, 그리고 보다 추상적인 관점에서 운영체제와 저장장치간의 교두보 역할을 하게 되는 파일 시스템도 마찬가지이다. 기존의 UFS에서 도스 기반의 FAT, 리눅스의 등장과 함께 그 발전을 함께한 ext 계열의 파일 시스템들이 바로 그 맥락에서 발전해온 파일 시스템들이다. 


파일 시스템은 사용자 어플리케이션을 대신하여 파일 레벨의 작업을 수행한다. 사용자 어플리케이션이 표준 라이브러리의 함수를 호출하여 특정 파일에 '쓰기'작업을 한다고 치면, 라이브러리는 커널의 write() 시스템 콜을 호출할 것이다. 커널은 해당 시스템 콜에서 타겟으로 하는 파일이 존재하는 파일시스템에게 write() 시스템 콜에서 정의되어 있는 루틴대로 작업을 시킬 것이고, 파일 시스템은 작업을 처리한 후 커널에게 다시 적절한 값을 리턴할 것이다. 최종적으로 사용자는 커널과 표준 라이브러리로부터 리턴값을 돌려받아 작업이 제대로 처리되었는지를 확인하게 될 것이다.


이는 파일 시스템이 하나만 존재하는 경우에만 성립한다 할 수 있다. 파일 시스템이 둘 이상 존재하게 될 경우, 모든 파일 시스템이 동일한 함수를 제공하지 않는 이상 시스템 콜도 그에 맞추어 서로 다른 루틴으로 작성되어야 할 것이다. 이렇게 되면 새로운 파일 시스템이 생길 때마다 새로운 시스템 콜이 작성되어야 할 것이고, 운영체제도 다시 컴파일 되어야 할 것이다.


이런 문제점을 해결하기 위해서 커널은 모든 파일 시스템이 지켜야할 일종의 표준 모델을 제시하였다. 앞으로 살펴볼 VFS상의 컴포넌트들이 바로 그것인데, 리눅스 커널은 이 모델을 정의하며 객체 지향 모델의 특징을 가져왔다. 하나의 자료구조에 관리를 위한 변수 뿐 아니라 함수의 포인터들의 선언으로 이루어진 테이블을 정의한 것이다. 다음은 표준 모델 중 하나인 struct file의 간략한 예시이다. 


struct file {

struct list_head f_list;

struct dentry * f_dentry;

struct vfsmount * f_vfsmnt;

struct file_operations * f_op;

...

};


struct file_operations {

int (*llseek) (struct file*, int, int);

int (*func2) (int, void *);

...

};


file_operations는 해당 함수의 원형을 정해놓고, 포인터의 형태로 선언한 것이다. 파일 시스템은 이러한 구조체로 제시된 함수의 형식에 맞추어 그 세부사항을 구현하면 된다. 그렇게 되면 커널은 VFS 수준에서 해당 파일시스템의 함수 포인터를 이용한 호출로 작업을 완료할 수 있게 된다. 마치 객체 지향 모델의 인터페이스(혹은 추상클래스)와 같은 모습이라 할 수 있겠다. 


이러한 표준 모형을 제시하기 위해서 VFS는 4개의 객체를 정의한다. 각각은 다음과 같다.

  1. super_block
  2. inode
  3. dentry
  4. file
이 다음부터는 각각의 자료구조와 역할에 대해서 알아보기로 한다.


'Tips > Kernel development' 카테고리의 다른 글

PID Namespace  (0) 2012.03.15
Linux Kernel : 커널이란  (0) 2012.01.31
Posted by 곰푼
Tips/Kernel development2012. 3. 15. 22:50
lwn.net의 PID Namespace 관련 글 하나를 번역해보았다.

Namespace는 커널 2.6.24버전부터 도입된 것으로, 일종의 독립적인 PID 공간이라고 볼 수 있다. Namespace들 간에 부모-자식 관계가 있으며, 각 Namespace별로 PID가 따로 관리되기 때문에 동일한 PID를 Namespace별로 사용할 수 있게 된다.

다음은 글의 원문이다.

A pid namespace is a "view" of a particular set of tasks on the system. They work in a similar way to filesystem namespaces. A file (or a process) can be accessed in multiple namespaces, but it may have a different name in each. In a filesystem, this name might be /etc/passwd in one namespace, but /chroot/etc/passwd in another.

For processes, a process may have pid 1234 in one namespace, but be pid 1 in another. This allows new pid namespaces to have basically arbitrary pids, and not have to worry about what pids exist in other namespaces. This is essential for checkpoint/restart where a restarted process's pid might collide with an existing process on the system's pid.
In this particular implementation, pid namespaces have a parent-child relationship, just like processes. A process in a pid namespace may see all of the processes in the same namespace, as well as all of the processes in all of the namespaces which are children of its namespace. Processes may not, however, see others which are in their parent's namespace, but not in their own. The same goes for sibling namespaces.

The know issue to be solved in the nearest future is signal handling in the namespace boundary. That is, currently the namespace's init is treated like an ordinary task that can be killed from within an namespace. Ideally, the signal handling by the namespace's init should have two sides: when signaling the init from its namespace, the init should look like a real init task, i.e. receive only those signals, that is explicitly wants to; when signaling the init from one of the parent namespaces, init should look like an ordinary task, i.e. receive any signal, only taking the general permissions into account.
The pid namespace was developed by Pavel Emlyanov and Sukadev Bhattiprolu and we eventually came to almost the same implementation, which differed in some details. This set is based on Pavel's patches, but it includes comments and patches that from Sukadev.
Many thanks to Oleg, who reviewed the patches, pointed out many BUGs and made valuable advises on how to make this set cleaner.



pid namespace는 시스템 상의 태스크의 특정 집합의 "View"이다. 
그들은 파일시스템의 namespace와 유사한 방법으로 동작한다. 파일(혹은 프로세스)은 다양한 namespace에서 접근될 수 있다. 그러나 파일(혹은 프로세스)은 각 namespace에서 다른 이름을 가져야 할 것이다. 파일시스템에서, 한 namespace에서 이름이 /etc/passwd가 될 수 있다. 하지만 다른 namespace에서는 /chroot/etc/passwd가 되어야 한다.
 프로세스들을 위해서, 프로세스는 한 namespace에서 pid 1234를 가질 수 있지만 다른 곳에서는 pid 1이 될 수 있다. 이는 다른 namespace에 어떤 pid가 있는지 고려하지 않고 새로운 pid namespace가 기본적으로 임의의 pid들을 가질 수 있도록 해준다. 이는 재 시작된 프로세스의 pid가 시스템의 pid상에서 존재하는 프로세스와 충돌할 때, 
checkpoint/restart를 위해서 필수적이다
.
 특정 구현에서, pid namespace는 마치 프로세스처럼 부모-자식 관계를 갖는다. pid namespace 내의 프로세스는 같은 namespace안의 모든 프로세스 뿐만 아니라 그 namespace의 자녀 관계인 namespace들 안의 모든 프로세스들도 볼 수 있을 것이다. 그러나 프로세스는 그들의 부모 namespace의 프로세스들을 볼 수 없을 것이고, 사촌관계에 있는 namespace의 프로세스들도 볼 수 없을 것이다.
 곧 해결될 이슈는 namespace 경계에서 시그널 처리이다. 이것은 현재 namespace의 init이 namespace에서 kill될 수 있는 일반적인 태스크처럼 다루어지기 때문이다. 이상적으로, namespace의 init에 의한 시그널 처리는 두 가지 측면을 가져야만 한다. : 그것의 namespace로부터 init에 시그널이 도착했을 경우, init은 실제 init 태스크처럼 보인다. 즉 시그널을 받았을 때, 그것은 확실히 받길 원하는 것이어야 한다. 부모 namespace의 하나로부터 init이 시그널을 받았을 때, init은 일반 태스크처럼 보여야 한다. 즉 어떤 시그널을 받던지, 일반적인 권한이 고려되어야 한다.
pid namespace는 Pavel Emlyanov와 Sukadev Bhattiprolu에 의해서 개발되었다. 우리는 거의 같은 사양으로 구현하기로 결정하였고, 몇몇 세부사항만이 다를 뿐이다. 이는 Pavel의 패치에 기초하였으나, 그것은 Sukadev로부터의 패치와 코멘트를 포함하고 있다.
원문주소 : http://lwn.net/Articles/245104/

'Tips > Kernel development' 카테고리의 다른 글

VFS : Virtual File System - Introduction  (0) 2012.09.30
Linux Kernel : 커널이란  (0) 2012.01.31
Posted by 곰푼