리눅스 커널

kenGwon·2024년 2월 19일
0

[Embedded Linux] BSP

목록 보기
17/36

수업자료ppt 5장의 9번째 ppt부터 시작하는 내용이다.

부트로더의 역할과 커널의 시작

커널이 한번 스타트가 되버리면 이제 더이상 부트로더는 의미가 없다.
커널이 스타트되면서 부트로더가 있던 영역을 덮어쓰면서 부트로더는 임무를 마치고 사라지게 된다.
애초에 부트로더의 역할은 부팅을 정상적으로 해서 커널을 작동시키는 것이었다.


리눅스 커널의 구조

  • IPC: 여러개의 프로세스 간의 통신이 필요하다. 그걸 위해 구현해놓게 IPC이다. 가장 기본적인것은 시그널이고, 소켓이나, 파이프, message queue, shared memory로도 IPC가 구현된다.
  • 가상 파일 시스템(Virtual filesystem): 메모리가 부족하면, 파일의 일부분을 마치 메모리처럼 사용한다.(swap memory) // 또는 디바이스를 모두 파일이라고 간주하면서 다루는 것도 가상파일시스템이라고 한다.
  • Process scheduler: 우분투는 250hz이고, 라즈베리파이는 100hz이다. 헤르츠에 따라서 프로세스 스케줄러는 컨텍스트 스위칭을 하게 된다. (자세한 내용은 아래를 참고) 즉 여러 프로세스가 CPU를 공평하게 사용할 수 있도록 해주는 것이다.
  • Network Interface: 표준 네트워크 프로토콜과 드라이버를 제공한다.
  • 메모리 관리(memory manager): 여러 개의 프로세스 메인 메모리를 안전하게 공유할 수 있도록 한다.

hz에 따른 context switching

여기서 말하는 hz는 kernel timer에 대한 hz를 말하는 것이다.
(CPU Clock hz랑 다른 것이다.)

우분투는 250hz로 4ms만 주고, 라즈베리파이는 100hz로 10ms만 준 이유는 두 머신의 컴퓨팅 파워가 다르기 때문이다.(우분투는 4ms만 줘도 엄청나게 많은 작업이 가능하고, 라즈베리파이는 우분투에 비해 컴퓨팅 파워가 좀 낮기 때문에 10ms는 줘야 우분투랑 좀 비슷한 수준의 작업량을 수행할 수 있다는 것이다.)

우분투 Hz: 250

커널 타이머 헤르츠로 250hz를 쓰겠다는 것은
f = 250hz라는 것이다. (250번 헤르츠가 진동하면서 기계어가 실행되는 동안 실행을 보장해주겠다는 것이다.)
T = 1/250 = 4ms의 시간동안 실행을 보장해주겠다는 것이다.
그 이후에는 강제적으로 문맥을 전환시켜주겠다는 것이다.

라즈베리파이 Hz: 100

ubuntu@ubuntu14:~/pi_bsp/kernel/linux$ vi .config

T = 1/100 = 10ms이므로 라즈베리파이는 하나의 프로세스에 대해 10ms 시간동안 실행을 보장해주겠다는 것이 된다.

time slice

4ms, 10ms가 타임 슬라이스 되어 프로세스에게 주어지는 한번의 단위 수행시간이라고 할 수 있겠다.


Process(Task)

RTOS에서는 Task라는 용어를 많이 사용하고, 리눅스에서는 Process라는 용어를 많이 사용한다.

전역변수인데 초기화되지 않은 변수들은 BSS 영역에 잡히게 된다.

코어가 여러개여야 "진짜"로 동시에 기계어가 실행될 수 있는 것이지, 멀티 프로세싱이라고 해서 진짜로 동시에 기계어가 실행되는 것은 아니다(컨텍스트 스위칭이 일어나면서 동시에 실행되는 것처럼 느껴질 뿐이다).

커널 프로세스 모델


스케줄러

스케줄러는 여러 프로세스가 CPU를 공유하여 사용 가능하도록 해준다.

preemptive & non-preemptive

리눅스

리눅스는 선점형과 비선점형이 좀 혼재되어있는 형태이다.

  • 프로세스 단위의 시각에서 보면 리눅스는 선점형이다.(우선순위가 높은 프로세스가 기존에 바로 다음에 기다리던 프로세스 앞에 들어오는 선점이 가능하다.)
  • 프로세스 내부 시각에서 보면 리눅스는 비선점형이다. 자기자신이 타임슬라이스로부터 부여받은 시간(위에서 보았던 4ms/10ms)동안은 오로지 자신의 코드를 실행하기 때문에 그 순간에는 선점할 수 없다. (Task preemption은 지원하지 않는다는 말이다. 즉 실행중인 프로세스는 선점하지 않는다는 것이다.) 그리고 리눅스에서 새로운 프로세스를 실행하기 위해서는 시스템 이벤트를 기다린다. (프로세스가 blocking되어 sleep 상태였다면, 시스템 이벤트를 받아서 프로세스가 다시 실행되게 된다.)

RTOS

그래서 보통 RTOS라고 하면 우선순위에 굉장히 민감해서 우선순위가 높은게 들어오면 그 어느순간에도 선점이 가능해진다.


Context Switch


CPU의 사용권을 다른 프로세스에게 양도한다. (현재의 Context 정보를 저장하고 새 프로세스의 Context 정보를 적재) --> 이러한 정보는 "task_struct" 구조체를 통해 관리된다.

context 정보

  • 프로그램 카운터(PC)
  • 스택 포인터(SP)
  • 범용 레지스터
  • 프로세서의 상태 레지스터
  • 메모리 관리정보

프로세스의 상태

프로세스의 주요 상태


좀비프로세스는 프로세스가 분명 종료 되었는데, 컨텍스트 정보가 남아있는 상태를 말한다. (이런 일이 발생하지 않으려면, 반드시 부모프로세스가 자식프로세스를 생성했다면 wait()함수를 통해서 자식프로세스의 자원을 회수해야 한다.)

프로세스 상태 전이도

  • 생성(created) 상태 : 커널 공간에 PCB등이 만들어지고 프로세스가 처음 생성되는 상태이다.
  • 준비(ready) 상태 : 기억 장치 등 필요한 모든 자원을 할당 받은 상태에서 프로세서를 할당 받으려고 대기하는 상태이다. 즉, 프로세서를 할당 받게되면 즉시 실행이 가능한 상태이다.
  • 실행(running) 상태 : 프로그램 코드가 프로세서에 의해 실행되고 있는 상태, 프로세스가 필요한 모든 자원을 할당 받은 상태이다.
  • 대기(blocked) 상태 : 프로세스가 필요한 자원을 요청하고 이를 할당 받을 때까지 기다리는 상태이다. (ex. 실행하다 I/O 등에 의해서 중단된 상태)
  • 지연 (suspended) 준비 상태 : 프로세스가 기억장치를 제외한 다른 모든 필요한 자원들을 보유한 상태이다.
  • 지연 (suspended) 대기 상태 : 프로세스가 대기 상태에서 기억 장치를 잃은 상태이다.
  • 종료(exit) 상태 : 프로세스의 실행을 종료하였으나 아직 프로세스에 대한 정보가 남아있는 상태이다. Zombie 상태라고도 한다.
  • 디스패치(dispatch) 또는 스케줄(schedule) :준비 상태에서 프로세서를 할당 받아 실행 상태로 전이하게 되는 이벤트이다.
  • 선점(preemption) 또는 time runout : 실행 상태의 프로세스가 프로세서 시간 할당량 끝나거나 우선순위가 높은 프로세스가 들어왔을 때 프로세서를 반납하고 준비상태로 전이하게 되는 이벤트이다.
  • 대기(block) : 실행 상태의 프로세스가 자원을 요청하여 대기 상태로 전이하게 되는 이벤트이다.
  • 웨이크업(wakeup) : 대기(지연대기) 상태에서 프로세스가 요청한 자원이 할당되어 준비(지연준비) 상태로 전이하게 되는 이벤트이다.
  • 스왑 인(swap-in) 또는 재활동(resume) : 지연준비(지연대기) 상태에서 기억장치를 할당받아 준비(대기) 상태로 전이하게 되는 이벤트이다.
  • 스왑 아웃(swap-out) 또는 지연(suspend) : 지연 (대기) 상태에서 기억장치를 잃어 지연 준비(지연 대기) 상태로 전이하게 되는 이벤트이다.

프로세스 생성

  • 경량급 프로세스 생성을 위한 clone() 시스템 콜
  • 전통적인 프로세스 생성을 위한 fork() 시스템 콜
  • 빠른 프로세스 생성을 위한 vfork() 시스템 콜

위 세개의 시스템 콜은 서로 다른 인자를 담아서 다시 한번 do_fork()라는 시스템 콜을 호출한다.
do_fork() 함수는 자식 프로세스를 생성하고 PID를 반환한다.

fork(), vfork(), // wait()

vfork()가 fork()와 가장 다른 점은 "부모프로세스의 text, data 세그먼트를 복사하지 않는다는 것"이다. 그렇기 때문에 빠른 프로세스 생성이 가능한 것이다.

fork()와 exec()의 만남

exec() 함수

호출된 새로운 프로그램으로 프로세스를 교체하여 실행

  • execl()
  • execlp()
  • execle()
  • execv()
  • execvp()
  • execvpe()

위 함수들에 대한 자세한 설명은 교재 277페이지를 참고하라

fork()와 exec()를 사용한 프로세스 생성 실습

fork()는 부모프로세스와 완전히 동일한 자식프로세스를 만들어낸다.
아래 코드는 자식 프로세스를 만들어서 "ls" 명령어를 실행하는 코드이다.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int fatal(char *s);

int main(void)
{
    pid_t pid;
    int status;

    switch (pid = fork())
    {
        case -1:
            fatal("fork failed.");
            break;
        case 0: /* 자식 프로세스 */
            sleep(60);
            execl("/bin/ls", "-l", "./test", NULL);
            fatal("exec failed.");
            break;
        default: /* 부모 프로세스 */
            //wait((int *)0); /* 부모는 자식이 종료될 때까지 대기 */
            wait(&status); /* 부모는 자식이 종료될 때까지 대기 */
            printf("ls completed %d\n", status >> 8);
            exit(0);
    }
}

int fatal(char *s)
{
    perror(s);
    exit(1);
}

<이해하기 포인트>

  • "부모 프로세스"의 PID가 2331137이다. 그리고 그 "부모 프로세스"를 실행시킨 부모프로세스인 bash의 PID가 2326955이다.
  • "자식 프로세스"의 PID가 2331138이다. 그리고 그 "자식 프로세스"를 실행시킨 부모프로세스의 PID는 위에서 보았던 2331137이다.

  1. 부모 프로세스의 PID가 1000라고 치자. 그러면 pid에는 1000이 들어간다.
  2. "switch (pid = fork())"에서 fork()가 정상적으로 실행되었다면, 부모 프로세스와 정확히 똑같은 자식 프로세스가 복제되어 생성된다.

    fork가 성공하면 child 프로세스를 생성 → parent 프로세스에게는 child 프로세스의 PID 반환 / child 프로세스에게는 0 반환

  3. 부모 프로세스의 경우 case -1, 1을 모두 건너뛰고 default로 가서 wait() 함수를 호출하면서 자식 프로세스의 PID인 0번을 wait하는 블로킹 상태로 진입한다.
  4. 그동한 새로 생성된 자식 프로세스는 fork()이후 부터 코드를 실행한다. 자신의 pid가 0이기 때문에 case 0: 에 진입하게 되고 execl() 함수를 실행하게 된다.

    그런데 child 프로세스도 같은 copy된 main프로세스를 실행하게 되니 pid_t pid = fork(); 가 또 실행되고 이게 무한히 반복되는 것은 아닌가? 하는 의문이 들었다.
    하지만 위에서 적은 것처럼 부모의 PCB도 fork()를 통해 자식에게 복사된다(PPID와 PID만 다르다).
    그리고 PCB에는 CPU에서 수행되던 레지스터의 값들이 저장되어 있고, 여기에는 Program Counter의 정보도 존재한다.따라서 child 프로세스에서도 fork 이후부터 코드가 실행된다.

  5. execl() 함수를 실행하는 순간 자식 프로세스는 나머지 코드 영역을 전부 덮어쓰면서 새로운 프로세스 코드를 생성한다. execl()이라는 시스템 콜 함수 안에 정의되어있는 내용만 실행한다는 것이다.
  6. 그래서 자식 프로세스에서 execl(); 함수가 정상적으로 실행되었다면, 그 다음에 오는 fatal(); 이라는 코드는 실행될 수 없게 된다. 기존 코드 내용을 execl()의 내용이 완전히 덮어썼기 때문이다.
  7. 그렇게 하여 자깃 프로세스가 execl() 함수를 통해 해야할 작업을 다 마치면 리턴하게 된다. (정상적으로 리턴되게 되면 리턴코드 0이 리턴된다. $? 같은 느낌)
  8. 그때 비로소 부모 프로세스의 wait(); 블로킹이 해제되면서, 이어지는 printf("ls completed\n"); 가 실행되게 된다. (wait()함수의 argument로 주어진 (int*)0은 자식 코드의 리턴 코드가 0일 때까지 기다리겠다는 말이 되겠다.)

[참고: fork에 대해 잘 정리된 블로그]

결과적으로 복제 후 실행(Fork() -> execl())이 가장 오버헤드가 적게 자식 프로세스를 생성하는 방법이다. 다시 말해, 그냥 새로 메모리 공간을 할당하여 새로운 프로그램을 로드하면서 자식프로세스를 생성하는 것보다 부모 프로세스를 복제하고 COW(Copy On Write) 하는 것이 마치 이미 만들어져 있는 탬플릿을 가져다 쓰는 느낌으로 동작하게 되어 훨씬 적은 오버헤드로 프로세스를 생성할 수 있다는 것이다.


커널 스레드 (kernel thread)

모든 프로세스의 조상은 1번 프로세스인 systemd 이다.

$ ps -ef | more

위 명령을 통해 많은 커널 스레드를 확인할 수 있다.

$ cd /etc/rc5.d
$ ls -al

위 명령을 통해 거기에 있는 파일들을 보면 리눅스가 부팅되면서 커널이 뜨고 init 프로세스가 생성되는데 필요한 파일 들이 담겨있다.

PID

각각의 프로세스를 유일하도록 구분하는 숫자 값

i-node

각각의 파일을 유일하도록 구분하는 숫자 값


메모리와 주소

  • 논리주소(Logical Address)와 선형주소(Linear Address, 가상주소 Virtual Address)랑 같은말이라고 봐도 된다.
  • 1기가 영역은 커널이 동작하는 영역이고, 3기가 영역은 사용자가 사용하는 영역이다.
  • CPU는 가상주소에서 동작한다고 봐야겠다.
  • 물리주소의 한계를 극복한 것이 가상주소이다. 물리주소가 1기가밖에 안되도, 가상주소 상으로는 4기가를 사용할 수 있다. 그래서 3기가의 남아있는 가상주소를 스왑메모리를 통해 사용하게 된다(파일 시스템의 공간을 메모리처럼 사용하겠다는 것).
  • 그러므로 당연히 물리주소와 가상주소는 1:1 매칭 될 수 없다. 이러한 역할을 해주는 것이 MMU(Memory Management Unit)이다.

cf)
만약 segmentation fault가 나서 exception이 난다면, "???" 명령을 통해서 core라는 파일을 만들도록 하고, GDB라는 프로그램을 통해서 core파일을 보면서 어디서 메모리 접근 오류가 나서 segmentation fault가 났는지 알 수 있다.

동적할당

컴파일 할 때는 얼마나 쓸지 알 수 도 없는 메모리에 대해서

객체지향에서 객체는 힙영역에 메모리가 잡히기 때문에 ㄱㅊ은데, 하지만 기본적으로 배열은 정적할당이 국룰이다.

다 사용했다면 free()함수를 통해서 반드시 반환해야 한다.

공유메모리 기법

커널에 공유 메모리 공간을 요청하고, 프로세스마다 그 공유 메모리에 접근하는 것이다.

system call "mmap()"

파일의 일부나 장치의 메모리를 프로셋의 주소 공간의 일부로 매핑하여 사용

사용자 영역과 커널 영역은 분명히 다른 메모리 영역에 있기 때문에, 메모리를 복사하여 가져다 써야한다. 그래서 예를 들어, 카메라의 영상을 커널에서 읽어와서 사용자 영역에 뿌릴 때 그 사이사이에 전부 복사 명령을 넣어주면 너무 오버헤드가 커진다. 그래서 바로 이러한 경우에, 사용자 영역에서 커널 영역의 메모리를 직접 접근하여 사용하게 되는데 그 때 사용하는 system call이 mmap()이다.


리눅스의 메모리 관리

라즈베리파이는 기본 스왑메모리가 되게 작게 잡혀있다. 그 스왑메모리를 확장해주면 나중에 OpenCV나 ROS를 빌드 할 때 한결 수월하게 할 수 있다.


물리 주소 공간의 조각화

세그멘테이션

  • 각각의 프로세스에 다른 선형주소(가상주소) 공간 할당
  • 프로그램을 코드, 전역데이터, 지역데이터 같은 논리적인 부분으로 쪼갠다.

페이지

  • 똑같은 선형주소(가상주소) 공간을 일정한 크기로 쪼개고, 다른 물리 주소 공간에 맵핑하여 사용
  • 페이지 프레임(page frame) 또는 물리적인 페이지(physical page)
    • 고정된 길이의 메모리
    • 각 페이지 프레임에 페이지가 하나씩 할당
    • 각 페이지는 고유의 번호를 부여 받는다.


Demand Paging

프로세스가 시작하면서 모든 페이지 프레임을 할당하여 사용하지 않고, 일부 페이지 프레임만 할당하여 사용하다가, 새로운 프레임 요구가 있을 때 새로운 페이지 프레임을 할당하여 사용하는 것..

이로 인해 똑같은 메모리의 양으로 더 많은 작업을 처리할 수 있다.

지역성 원리(Locality Principle)

프로세스가 처음부터 자신의 메모리 공간에 있는 모든 영역을 접근하지 않는다.
프로그램은 실행되는 각 단계에서 보통 프로세스 페이지의 일부만을 사용한다.


스와핑(Swapping)

디스크의 일부 공간을 램의 확장으로 사용하는 것

swap-out

  • 커널 쓰레드 kswapd

    • 빈 페이지 프레임이 미리 정의한 기준치 보다 작아질 때 마다 1초 간격으로 스왑 아웃 수행
  • 스왑 아웃되는 페이지

    • 가장 많은 페이지를 소유한 프로세스의 페이지를 회수
    • LRU(Least Recently Used)
      • 가장 오랜 시간 동안 사용 하지 않는 페이지를 스왑 아웃

인터럽트 핸들러의 분리

Top half

  • 하드웨어 레벨의 인터럽트 핸들러
  • do_IRQ()함수에서 처리

Bottom half

  • 소프트웨어 레벨의 인터럽트 핸들러로 안정된 시점에 실행된다.

리눅스 타이머의 활용


변수 jiffies

jiffies라는 변수를 이용하여 시스템이 시작하고 나서부터 지나간 틱수를 기록한다.
커널 초기화 때 0으로 설정되고 타이머 인터럽트가 발생할 때마다 1씩 증가한다.


VFS와 리눅스 파일 시스템

Common File 모델

  • 전형적인 유닉스 파일시스템에서 제공하는 모델 사용
    mount, read, write, ioctl, ...
  • 특정 파일 시스템을 구현할 때 자신의 물리적인 구조를 공통 파일 모델로 변환
  • 각 디렉토리는 파일 리스트와 하위 디렉토리를 포함한 일반 파일로 간주

Common File 모델의 객체 유형

  1. 슈퍼블록(Superblock) 객체
    : 마운트 시킨 파일 시스템 정보를 저장
  2. 아이노드(i-node) 객체
    : 특정 파일에 대한 일반 정보를 저장
  3. 파일(file) 객체
    : 열린 파일과 프로세스 사이의 상호작용과 관련한 정보를 저장
  4. 디엔트리(dentry) 객체
    : 디렉토리 항목과 대응하는 파일 간 연결에 대한 정보 저장
profile
스펀지맨

0개의 댓글