본문 바로가기
OS

커널 구조

by 진득한진드기 2026. 2. 19.

태스크 관리

 

실행 파일 이란 애초에 그냥 디스크에 있는 실행 가능한 수동파일이다.

이 수동적인 녀석이 실행되려면 커널로부터 CPU 등의 자원을 할당받는 동적인 객체가 되어야하는데 이 동적인 객체가 프로세스이다.

프로세스 자신의 독립적인 자원(메모리 공간)을 가지게 되고, 쓰레드는 부모와 자원을 공유하기 때문에 결함이 발생하면 부모까지 영향을 끼친다

fork()와 vfork()의 차이는 둘다 프로세스를 생성하지만 vfork()의 경우 일단 같은 주소공간을 가르킨다. execl은 기존에 사용하던 프로세스의 주소공간을 모두 없애고 요청된 바이너리를 기반으로 새로운 주소 공간을 생성한다.

그래서 fork()이후 execl()이 되었다면결국 fork() 때 수행되었던 부모 프로세스의 주소 공간을 복사하여 자식 프로세스의 주소공간을 따로 만들어 주기 때문에 중간에 개인 자원을 할당한게 불필요한 작업이 되기 때문에 vfork()가 존재한다. 이를 위해 COW(Copy on Write)기법을 도입하여 fork()할 때 야기 되는 공간 복사 비용을 줄였다.

 

커널 안에서의 쓰레드와 프로세스


커널은 사실상 프로세스와 쓰레드중 어떤게 요청 되더라도 비슷하게 처리한다.

task_struct라는 구조체를 사용해서 관리를 동일하게 하기 때문이다. 실행 이미지를 공유하는지 같은 쓰레드 그룹에 있는가를 통해서 차이가 있을뿐...

태스크 관리자가 관리하는 자원을 어떻게 공유하고 접근 제어하느냐에 따라 프로세스나 쓰레드로 해석이 다르게 된다.

사용자가 쓰레드를 생성을 요청하면 시스템 호출을 통해 리눅스 커널에 전달된다.
전달 되는 제어흐름은 fork() -> fork() -> clone() -> sys_clone() 처럼 vfork() 와 clone()모두 조금씩은 다르지만 커널 내부 함수인 do_fork()를 호출하는건 같다.

시스템에 존재하는 태스크는 유일하게 구분이 되어야하므로 태스크 별로 유일한 값은 task_struct 구조체내의 pid 필드에 있다.

한 프로세스 내의 쓰레드는 동일한 PID를 공유해야하므로 tgid(Thread Group ID)라는 개념을 도입하여 task_struct안에서 해당 부모 태스크와 자식 태스크가 같은 프로세스안에 있다고 판단이 가능해지는 것.

 

쓰레드 상태전이와 실행 수준 변화

 

태스크는 자기 일을 수행하면서 디스크나 락등 CPU 이외의 자원을 요청하는데 태스크는 이때 잠시 대기 하도록 만든뒤 다른 태스크를 먼저 수행시키며 태스크가 요청했던 자원이 사용가능해지면 수행 시켜 시스템 활용율을 높인다.

태스크가 생성되면 그 태스크는 준비 상태(TASK_RUNNING)가 된다.

스케줄러는 이를 선택해서 실행한다. 그래서 CPU가 여러개인 컴퓨터면 여러개가 실행이 가능하다.

부모 태스크가 자식 태스크보다 빨리 없어져서 자식 태스크가 좀비가 되면 init 태스크(프로세스)가 가져가서 삭제시킨다.

시그널을 받은 태스크는 추후 SIGCONT 시그널을 받아 다시 준비 상태로 전환된다.  만약에 ptrace()호출에 의해 디버깅 되고 있는 태스크는 시그널을 받는 경우 TASK_TRACED상태로 전이 될 수 있다.

 

트랩


커널 수준 실행 상태로 전이하는 방법은 2개가 있는데

1. 시스템 호출 사용하면 커널에 트랩이 걸리게 되고 그 결과 태스크의 상태가 커널 수준 실행상태로 전이된다.
2. 인터럽트 발생 시스템 호출과 마찬가지로 인터럽트가 발생하면 리눅스 커널에 인터럽트가 걸리는데 이때 실행중이던 태스크가 사용자 수준에서 동작하고 있었다면 커널 수준 실행 상태로 전이되고, 커널읜 인터럽트 처리 루틴으로 제어가 넘어가게된다.

사용자 수준 수행과 커널 수준 수행의 존재는 좀더 복잡한 스태 관리를 요구하는데, 커널 수준 코드는 바로 리눅스 그자체이다. 리눅스도 소프트웨어 이므로, 스택을 필요로 하는데 이 스택을 활용하기 위해서 리눅스는 태스크별로 8KB나 16KB의 스택을 할당해준다. 

간단하게 보면 A라는 태스크가 있다고 하면 거기서 시스템 호출을 하면 A에게 할당해준 커널 스택을 사용하여 요청된 작업을 수행한다.

결론적으로 task_struct 구조체와 커널 스택을 할당하게 된다.

태스크당 할당되는 커널 스택은 thread_union 이라 불리며, thread_info 구조체를 포함하고 있다.

struct pt_regs {
    long ebx;
    long ecx;
    long edx;
}



커널과 사용자 수준간의 변화 시에 현재까지의 작업 상황을 저장해놓아야하는데 위 구조체가 사용된다.
커널로 진입되는 시점에 커널 스택안에 현재 레지스터의 값들을 구조체를 이용하여 일목요연하게 저장함으로써 이뤄진다.

 


태스크 스케줄(런 큐)

 

태스크가 처음 실행되면 init_task를 헤드로 하는 이중연결 리스트를 생성한다.

여기서는 우리가 핀토스 때 배웠던 wake_up()함수와 ready_queue(list)로 들어가게되어 작업이 진행되는것이다. 

여러개의 CPU를 가진 모델이라면 이런 런큐가 여러개이다.

만약에 프로세스를 생성한다면 자식 프로세스는 부모 프로세스와 같은 런큐로 삽입된다. 캐시 친화율을 위해서 이다.

일반 태스크는 CFS을 사용하고 실시간은 FIFO, RR이나 DEADLINE 정책을 제공한다. 

다중 모델에서 CPU 런큐가 불균등할 경우 부하균등(loadbalandcing) 을 사용하여 테스크를 이주(migration)시킨다.

그리고 이주를 결정했으면 어떤 CPU로 이주시킬것인가? -> 하이퍼 쓰레딩을 지원하는 시스템에서는 이주 성능을 높이기 위해서 각자 붙어있는 모델의 큐를 사용한다.

만약에 그게 불가능하다면 L3 캐시를 공유하고 있는 큐로 이주 시킨다.

### 태스크와 시그널

시그널은 태스크에게 비동기적인 사건의 발생을 알리는 매커니즘이다.

태스크가 시그널을 원활히 처리하려면 다음과 같은 3가지 기능을 지원해야하는데

1. 다른 태스크에게 시그널을 보낼수있어야한다.
2. 자신에게 시그널이 오면 그 시그널을 수신할 수 있어야 한다. 이를 위해 signal, pending이라는 변수가 존재한다.
3. 자신에게 시그널이 오면 그 시그널을 처리할 수 있는 함수를 지정할 수 있어야 한다. 이를 위해 sys_signal()이라는 시스템 호출이 존재한다.


예시로 kill pid 를 사용한다면, pid를 가지고 있는 태스크를 종료할것이다.

이때 사용자는 pid를 공유하고 있는 쓰레드들이 모두 종료할 것이라 생각할 것이다.

리눅스에서는 PID는 실제로는 tpid를 의미하므로  따라서 PID를 공유하고 있는 태스크들은 의미상 같은 쓰레드 그룹이므로, PID를 공유하고 있는 모든 쓰레드들 간에 시그널 또한 공유해야한다.

이렇게 여러 태스크들 간에 공유해야하는 시그널이 도착하게 되면 이를 task_struct 구조체의 signal필드에 저장해둔다.

이런 시그널을 보내는 작업은 sys_kill() 을 통해서 한다.

반대로 특정 태스크에게만 시그널을 보내야 하는 경우라면 어떨까?

공유하지 않는 시그널은 task_struct 구조체의 pending 필드에 저장해둔다.

시그널을 signal 필드나 pending 필드에 저장할 때는 시그널 번호 등을 구조체로 정의하여 큐에 등록시키는 구조를 택하고 있으며, 이를 위해 sys_tkill()과 같은 시스템 호출을 도입하였다.

한편 각 태스크는 특정 시그널이 발생했을 때, 수행될 함수 즉 , 시그널 핸들러를 지정할 수 있다.
이 때 사용자 지정 시그널 핸들러를 설정하게 해주는 함수가 sys_signal()이다.

태스크가 지정한 시그널 핸들러는 task_struct 구조체의 sighand 필드에 저장된다.



또한 태스크는 특정 시그널을 받지 않도록 설정할 수 있는데, 이는 task_struct 의 blocked필드를 통해 이루어진다.

SIGKILL이랑 SIGSTOP은 무시가 불가능하고, 그외들은 가능하다.

수신한 시그널의 처리는 태스크가 커널수준에서 사용자 수준 실행 상태로 전이할 때 이루어진다. 즉 커널은 pending 필드의 비트맵이 켜져있는지 ,혹은 signal필드의 count가 0이 아닌지 검사를 통해 처리를 대기 중인 시그널이 있는지 확인 할 수 있다.

0이 아니면 어떤 시그널이 대기중인지 검사하고, 이 시그널이 블록되어있지 않다면 시그널 번호에 해당되는 시그널 핸들러를 sighand필드의 action 배열에서 찾아서 수행시켜주게 된다.

인터럽트, 트랩과 시그널의 차이라면 인터럽트 트랩은 커널에게 알리는것이라면, 시그널은 태스크에게 알리는 것이다.