본문 바로가기
정보과학/운영체제특론

프로세서 관리 커널 1

by J1소프트 2023. 12. 13.
728x90

1. 프로세스 기초
   프로세스(process)는 모든 멀티프로그래밍 운영체제의 필수 개념입니다. 이 프로세스는 태스크(task)와 같은 개념으로 사용되기도 합니다. 본 강의에서는 프로세스와 태스크를 구분하지 않고 사용하도록 하겠습니다.


1) 프로세스
   ․프로세스(process)는 모든 멀티프로그래밍(multiprogramming) 운영체제의 필수 개념이다. 운영체제 연구자들은 프로세스를 프로그램의 수행 환경, 스케줄링의 단위, 주소 공간과 제어 흐름의 집합, PCB(Process Control Block)가 존재하는 객체 등 여러 가지로 정의하였다.  
   ․일반적으로는 실행 상태에 있는 프로그램의 인스턴스(instance)로 정의한다. 
    -프로그램(program)은 디스크에 실행 가능한 형태로 저장되어 있는 기계어 명령과 자료의 집합으로 이 자체는 수동적인 존재이다.  
    -프로세스는 동작중인 프로그램으로 처리기가 기계어 명령들을 실행함에 따라 끊임없이 변화하는 동적인 존재이다.  
   ․프로세스는 태스크(task)와 같은 의미로 사용하기도 한다. 
    한편 Mach 운영체제를 설계했던 CMU(Carnegie Mellon University)의 연구자들은 프로세스를 태스크와 스레드들의 집합으로 정의한다. 일부 리눅스 서적에서는 동작중인 응용을 프로세스라고 정의하고, 그 프로세스를 관리하기 위한 커널 내부 객체를 태스크라고 정의하기도 한다. 

 

2) 경량프로세스
   ․리눅스는 어플리케이션 자료구조의 대부분을 공유하면서 서로 독립적인 다수의 실행 흐름으로 이루어진 사용자 프로그램, 즉 멀티스레드 어플리케이션(multithreaded application)을 지원한다.  
   ․프로세스는 여러 사용자 스레드(user thread, 간단히 스레드라고도 한다.)로 이루어지고, 각 스레드는 프로세스의 실행 흐름을 나타낸다. 요즘은 표준 라이브러리인 pthread(POSIX 스레드 : Portable Operating System based on Unix thread) 라이브러리 함수를 이용해서 대부분의 멀티스레드 어플리케이션을 작성한다. 
   ․리눅스는 멀티스레드 어플리케이션을 잘 지원하기 위해 경량 프로세스(lightweight process)를 사용한다. 
    -기본적으로 두 개의 경량 프로세스는 주소 공간이나 열린 파일 등 여러 자원을 공유 할 수 있다.  
    -둘 중 하나가 공유하는 자원에 어떤 변경을 가하면 다른 쪽도 바뀐 부분을 바로 알 수 있다.  
    -당연히 두 프로세스는 공유 자원을 접근할 때 서로 동기화를 해야 한다.  
   ․경량 프로세스를 사용할 수 있다면 각 스레드를 경량 프로세스와 연계함으로써 멀티스레드 어플리케이션을 수월하게 구현할 수 있다. 이렇게 스레드는 간단히 같은 메모리 주소 공간과 같은 열린 파일 등을 공유함으로써 같은 어플리케이션 자료 구조에 접근할 수 있다. 
   ․동시에 커널은 각 스레드를 서로 독립적으로 스케줄링할 수 있기 때문에 한 스레드가 잠든 상태더라도 다른 스레드는 실행 상태로 있을 수 있다. 
   ․리눅스의 경량 프로세스를 사용하는 POSIX 호환 pthread 라이브러리의 예로는 리눅스 스레드(Linux Thread)와 IBM에서 발표한 차세대 POSIX 스레딩 패키지(NGPT, Next Generation Posix Threading Package)가 있다. 

 

3) 커널 스레드
(1) 커널 스레드
   ․모든 프로세스의 조상은 프로세스 0 또는 스와퍼 프로세스(swapper process)라고 부르며, 이것은 start_kernel() 함수가 리눅스를 초기화하는 과정에서 맨 처음 만드는 커널 스레드(kernel thread)다.  
   ․프로세스 0은 TASK_RUNNING 상태에 있는 다른 프로세스가 없는 경우에만 스케줄러에 의해 선택된다.  

 

(2) start_kernel() 함수 
   ․tart_kernel() 함수는 리눅스 커널을 초기화하는 것으로 거의 모든 커널 구성 요소를 초기화한다. 이 중 중요한 몇 가지만 설명하면 다음과 같다. 
① paging_init() 함수를 호출하여 페이지 테이블을 초기화한다. 
② kmem_init(), free_area_init(), mem_init() 함수를 호출하여 페이지 디스크립터를 초기화한다.  
③ trap_init() 함수와 init_IRQ() 함수를 호출하여 IDT(Interrupt Descriptor Table)를 최종적으로 초기화한다.  
④ kmem_cache_init()와 kmem_cache_sizes_init() 함수를 호출하여 슬랩 할당자를 초기화한다.  
⑤ time_init() 함수를 호출하여 시스템 날짜와 시간을 초기화한다.  
⑥ kernel_thread() 함수를 호출하여 init 프로세스로 잘 알려진 프로세스 1이라는 다른 커널 스레드를 생성한다. 프로세스 1은 PID 1이며, 프로세스마다 할당하는 모든 커널 자료 구조를 프로세스 0과 공유한다. 스케줄러가 프로세스 1을 선택하면 이는 init() 함수를 실행한다. 또한 이 프로세스는 운영체제의 바깥 계층을 구현하는 모든 프로세스를 생성하고 동작을 감시하기 때문에 시스템을 종료할 때까지 살아 남는다.  

 

(3) 리눅스에서 커널 스레드와 정규 프로세스의 차이
   ․리눅스에서 커널 스레드는 다음과 같은 점에서 정규 프로세스와 다르다. 
① 각 커널 스레드는 커널의 특정 C 함수 하나만 실행하지만, 정규 프로세스는 시스템 콜을 통해서만 커널 함수를 실행한다.  
② 커널 스레드는 커널 모드에서만 동작하지만, 정규 프로세스는 커널 모드와 사용자 모드를 번갈아 가며 동작한다. 
③ 커널 스레드는 커널 모드에서만 동작하므로 PAGE_OFFSET보다 큰 선형 주소만 사용한다. 반대로 정규 프로세스는 4GB 선형 주소 모두를 사용자 모드나 커널 모드에서 사용한다.  

 

(4) 기타 커널 스레드
   ․이 외에도 여러 커널 스레드를 활용한다. 이 중 어떤 것은 초기화 단계에서 생성하여 시스템을 종료할 때까지 실행하고, 어떤 것은 커널이 별도의 실행 문맥에서 실행하면 더 좋은 성능을 내는 작업을 실행해야 할 때 필요에 의해 생성한다. 중요한 커널스레드를 몇가지 더 살펴보면 다음과 같다. 
① keventd : qt_context 작업 큐(task queue)에 있는 작업을 실행한다.  
② kapmd : 고급 전원 관리(APM : Advanced Power Management)와 관련 있는 사건을 처리한다.  
③ kswapd : 메모리를 해제한다.  
④ kflushd : bdflush라고도 하며, 더티(dirty) 버퍼를 디스크에 저장해서 메모리를 지운다. bdflush라고도 하며, 더티(dirty) 버퍼를 디스크에 저장해서 메모리를 지운다.  
⑤ kupdated : 오래된 더티 버퍼를 디스크에 기록해서 파일시스템의 데이터가 잘못될 위험을 줄인다.  
⑥ ksoftirqd : 소작업(tasklet)을 실행한다. 시스템에 있는 각CPU마다 이커널스레드가 하나씩 있다.  

 

4) 문맥 교환(context switch)     
 ▶ 커널은 프로세스의 우선 순위, 프로세스가 실행 상태에 있는지 아니면 어떤 사건을 기다리며 블록 상태에 있는지, 프로세스에 어떤 주소 공간이 할당되어 있는지, 어떤 파일을 다룰 수 있는지 등을 알아야 한다.     
 ▶ 프로세스 디스크립터(process descriptor) 즉 한 프로세스와 관련된 모든 정보를 담고 있는 task_struct 자료 구조의 역할이다.     
▶ 커널 관점에서 보면 프로세스의 목적은 시스템 자원(CPU 시간이나 메모리 등)을 할당받는 존재로서 동작하는 것이기 때문에, 프로세스를 프로그램의 실행이 얼마나 진행되었는지를 완전하게 기술하는 자료 구조의 집합이라 볼 수 있다. 

 

(1) task_struct
   ․task_struct 자료 구조의 는 프로세스 디스크립터(process descriptor)로 한 프로세스와 관련된 모든 정보를 담고 있다.


  *커널은 프로세스가 생성될 때 프로세스의 정보들을 관리하기 위해 프로세스 디스크 립터 즉 task_struct 등을 비롯한 많은 자료 구조들을 할당한다.  
  *프로세스를 위한 자료 구조들 중에서 가장 중심적인 위치에 있는 구조는 task_struct 이다. 
  *이 자료 구조는 include/linux/sched.h 파일에 정의되어 있다. task_struct는 각 프로 세스마다 하나씩 존재한다. 즉 새로운 프로세스가 생성될 때마다 하나씩 할당된다.  
  *운영체제 연구자들은 이러한 자료 구조들을 문맥(context)이라고 부른다.  

 

(2) 프로세스 문맥(context)
   ․문맥(context) 은 새로운 프로세스가 생성될 때마다 task_struct가 하나씩 할당되는 것을 말한다.
   ․프로세스의 문맥은 크게 세 부분으로 구분할 수 있다. 


   ● 프로세스의 정보를 유지하기 위해 커널이 할당한 자료 구조
      프로세스의 문맥의 첫 번째 부분은 프로세스의 정보를 유지하기 위해 커널이 할당한 자료 구조들이다. 대표적인 자료 구조로는 태스크 구조(task_struct), 세그먼트 테이블, 페이지 테이블, 파일 디스크립터, 파일 테이블 등이 있다. 

 

   ● 프로세스의 수행 이미지
      프로세스의 문맥의 두 번째 부분은 프로세스의 수행 이미지(프로세스를 구성하는 명령어들과 데이터들)이다. 프로세스의 수행 이미지는 코드(cord), 데이터(data), 스택(stack), heap 공간 등으로 구성되며, 이들의 일부는 주 기억 장치에 그리고 다른 일부는 디스크(스왑 공간 또는 파일 시스템)에 존재한다. 

 

   ● 스레드(thread) 구조 또는 하드웨어 문맥(hardware context)
      프로세스의 문맥의 세 번째 부분은 문맥 교환(context switch)할 때 프로세스의 현재 실행 위치에 대한 정보를 유지하는 곳으로 스레드(thread) 구조 또는 하드웨어 문맥(hardware context)이라고 불리는 부분이다. 이 부분은 실행 중이던 프로세스가 대기 상태나 준비 상태로 전이할 때 이 프로세스가 어디까지 실행했는지 기억해 두는 공간으로, 이후 이 프로세스가 다시 실행될 때 기억해 두었던 곳부터 다시 시작하게 된다. 

 

(3) 문맥 교환의 개념
   ․프로세스 실행을 제어하기 위해 커널은 CPU에서 실행 중인 프로세스의 실행을 멈추고 이전에 멈춘 다른 프로세스의 실행을 재개할 수 있어야 한다.
   ․이러한 동작을 프로세스 교환(process switch), 또는 태스크 교환(task switch), 문맥 교환(context switch)이라고 한다. 

 

(4) 리눅스에서의 문맥 교환
   ․리눅스에서 문맥교환과 관련한 요소로는 하드웨어 문맥이 있다. 
   ․하드웨어 문맥은 CPU에서 프로세스 실행을 재개하기 전에 레지스터로 다시 복구해야 하는 집합이고, 프로세스 실행에 필요한 모든 정보를 포함하는 프로세스 실행 문맥(execution context)의 일부다.
   ․리눅스에서는 문맥 교환을 소프트웨어적으로 처리하기 때문에 하드웨어 문맥을 작업 상태 세그먼트(TTS : Task State Segment)에 저장하지 않고, 일부는 프로세스 디스크립터에 그리고 나머지는 커널 모드 스택에 저장한다. 또한 문맥교환은 커널 모드에서 schedule() 함수를 통해서만 일어날 수 있다. 프로세스가 사용자 모드에서 사용하던 모든 레지스터의 내용은 문맥을 교환하기 전에 저장된다.

 

2. task_struct 자료구조
   Task_struct 자료구조는 각 프로세스마다 존재하며 실행과 관련된 모든 정보를 포함하게 되며, 새로운 프로세스가 생성될 때마다 하나씩 할당되게 된다.
 
1) identification
(1) 프로세스를 식별하기 위한 변수
   ․프로세스 ID를 나타내는 pid, 프로세스의 그룹 ID인 pgrp, 세션 ID인 session 등의 변수가 있다. pid 숫자는 순차적으로 할당한다. 
   ․새로 만든 프로세스의 pid는 보통 바로 전에 만든 프로세스의 pid에 1을 더한 값이다. 그러나 16비트 하드웨어 플랫폼에서 개발한 고전 유닉스 시스템과 호환성을 유지하기 위해 리눅스에서 사용할 수 있는 최대 pid 숫자는 0˜32767이다.
   ․커널은 32768번째 프로세스를 만들 때 사용하지 않는 낮은 pid 숫자를 재활용하여 번호를 매기기 시작한다. 리눅스는 시스템에 있는 각각의 프로세스나 경량 프로세스에 다른 pid를 부여한다. 

 

(2) 스레드 그룹(thread group)
   ․리눅스 2.4에서는 표준과 호환성을 유지하기 위해 스레드 그룹(thread group)이라는 개념을 도입했다.
   ․스레드 그룹은 본질적으로 멀티스레드 어플리케이션의 스레드에 해당하는 경량 프로세스의 모음이다.
   ․task_struct의 thread_group 변수에 같은 스레드 그룹에 들어 있는 모든 경량 프로세스의 디스크립터를 이중 연결 리스트로 모은다. 
   ․스레드는 그 그룹에 있는 첫 번째 경량 프로세스의 pid를 식별자로 공유하며, 이를 task_struct의 tgid 변수에 저장한다. getpid() 시스템 콜은 current->pid가 아닌 current->tgid를 반환한다. 따라서 멀티스레드 어플리케이션의 모든 스레드는 같은 식별자를 공유한다. 일반 프로세스나 스레드 그룹에 들어가지 않는 경량 프로세스의 경우 tgid 변수는 pid 변수와 값이 같다.  
   ․위에서 current는 매크로로서, 커널이 esp 레지스터 값을 이용하여 CPU에서 현재 실행 중인 프로세스의 task_struct 주소를 쉽게 구할 수 있도록 한다.  
   ․실제로 프로세스에 할당된 메모리 영역의 크기는 8KB이므로, 커널은 esp 레지스터의 하위 13비트만 지우면 task_struct의 시작 주소를 구할 수 있다.  

 

(3) 프로세스에 대한 사용자 접근권한을 제어할 때 이용되는 변수
   ․파일과 이미지에 대한 접근 권한을 검사하기 위해 사용자 그룹과 그룹 식별자를 사용한다.  
   ․리눅스 시스템의 모든 파일들은 소유권과 접근 권한을 가지며, 접근 권한은 사용자들이 파일이나 디렉토리에 대한 접근 방식을 다룬다.  
   ․기본적인 권한들은 읽기, 쓰기, 실행으로 파일의 소유자, 특정 그룹에 속하는 프로세스들, 시스템의 모든 프로세스들의 세 가지 종류의 사용자에 할당된다. 각각의 사용자 계층은 각기 다른 권한을 가질 수 있다. 

※ 프로세스의 task_struct에는 다음과 같은 네 쌍의 사용자 식별자와 그룹 식별자가 있다. 
   uid와 gid : 프로세스를 실행시킨 사용자의 사용자 식별자와 그룹 식별자.  
   euid와 egid : 어떤 프로그램은 uid와 gid를 프로세스를 실행시킨 사용자의 것으로부터 자신의 것으로 변화시킬 수 있다. 이러한 프로그램은 setuid 프로그램으로 알려져 있으며, 이런 프로그램은 특히 네트워크 데몬과 같이 다른 프로세스의 한켠에서 실행되고 있는 서비스의 권한을 제한하기 위한 유용한 방법이 된다. euid와 egid는 setuid 프로그램의 uid와 gid이며, 원래의 uid와 gid는 그대로 남는다. 커널은 특권 권한을 검사할 때 euid와 egid를 검사한다. 
 

  suid와 sgid : 이는 POSIX 표준의 요구사항에 따른 것이며 시스템 콜을 이용하여 프로세스의 uid와 gid를 바꾸는 프로그램이 사용한다. 원래의 uid와 gid가 바뀌어 있는 동안 실제 uid와 gid를 저장하는데 사용된다.    
   fsuid와 fsgid : 이것은 euid와 egid와 거의 같으며, 파일 시스템의 접근 권한을 검사할 때 사용된다. NFS 마운트된 파일시스템에서 사용자 모드인 NFS 서버가 파일을 접근할 때 서버로서가 아니라 특정 프로세스로서 파일을 접근해야 하기 때문에 필요하다. 이러한 경우에는 파일 시스템 uid와 gid만 변경된다.(euid와 egid는 변경되지 않는다.) 이렇게 함으로써 악의를 가진 사용자가 NFS 서버에게 kill 시그널을 보낼 수 있게 되는 것을 막는다. kill 시그널은 euid와 egid를 가진 프로세스에게만 전달된다.  

 

2) state
(1) State 필드의 특성
   ․이 필드는 프로세스에 현재 무슨 일이 벌어지고 있는지를 나타낸다.  
   ․플래그의 배열로 구성되며, 각 플래그는 가능한 프로세스 상태를 나타낸다. 각 상태는 상호 배타적이기 때문에, state에 플래그를 하나 설정하면 나머지 플래그는 모두 지워진다.  

 

(2) 가능한 프로세스 상태 및 설명
   ․ 다음은 가능한 프로세스 상태를 도시화하고 그에 대한 설명이다.
    *TASK_RUNNING : 프로세스가 CPU에서 실행 중이거나, 실행되기를 기다리는 중이다. 즉 프로세스가 실행 상태이거나 준비 상태를 나타낸다.(준비 상태와 실행 상태는 서로 다른 상태이다. 그러나 리눅스 커널은 일반적인 UNIX와는 다르게 실행 상태와 준비 상태를 TASK_RUNNING 이라는 하나의 상태 값으로 표현한다.) 

 

    *TASK_INTERRUPTIBLE : 프로세스가 어떤 조건이 맞아떨어지기를 기다리며 대기 중(잠들어 있는 중)이다. 프로세스를 깨울 수 있는 조건으로는 하드웨어 인터럽트가 발생하거나, 프로세스가 기다리고 있는 자원이 해제되거나, 프로세스에 시그널을 전달하는 것이 있을수 있다.(프로세스는 깨어나면 TASK_RUNNING상태로 되돌아간다.) 

 

    *TASK_UNINTERRUPTIBLE : TASK_INTERRUPTIBLE 와 비슷하지만 잠들어 있는 프로세스에 시그널을 전달해도 프로세스 상태가 바뀌지 않는다는 차이가 있다. 이 프로세스 상태는 거의 사용하지 않는다. 그러나 프로세스가 정해진 사건이 발생하기를 기다리는 도중에 방해받으면 안 되는 특수한 상황에서 유용하다. 예를 들어 프로세스가 장치 파일을 열 때 해당 장치 드라이버가 자신이 다룰 하드웨어 장치가 있는지 조사하는 경우에 이 상태를 사용할 수 있다. 장치 드라이버는 조사를 완료할 때까지 방해받으면 안 된다. 그렇지 않으면 하드웨어 장치가 예측할 수 없는 상태에 빠질 수도 있다. 

 

    *TASK_STOPPED : 프로세스가 시그널이나 트레이싱 등의 이유로 잠시 중단되었음을 나타낸다. 프로세스는 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 시그널을 받으면 이 상태가 된다. 특정 프로세스가 다른 프로세스를 감시하고 있을 때(예를 들면 디버거가 ptrace() 시스템 콜을 호출하여 다른 프로그램을 감시하는 경우) 시그널은 프로세스를 TASK_STOPPED 상태로 만들 수 있다. 
 

   *TASK_ZOMBIE : 프로세스 실행은 종료했지만 부모 프로세스가 wait() 계열 시스템 콜(wait(), wait3(), wait4(), waitpid())을 호출하여 종료한 프로세스의 정보를 반환하지 않은 경우다. 부모 프로세스가 wait() 계열 시스템 콜을 호출하기 전에는 종료한 프로세스의 프로세스 디스크립터에 들어 있는 데이터를 부모 프로세스가 필요로 할 수 있기 때문에 커널은 이 데이터를 없애서는 안 된다. 

 

3) task relationship

 

(1) Task relationship 필드의 특성
   ․리눅스에서 다른 프로세스와 무관한 프로세스는 없다. 최초의 프로세스를 제외한 모든 프로세스는 반드시 부모 프로세스를 가지며, 형제나 자식 프로세스를 가질 수 있다.  
   ․새로운 프로세스가 생성된다는 것은 이전의 프로세스로부터 복사(copy) 혹은 복제(clone)된다는 것을 의미한다.  

 

(2) Task relationship의 포인터 변수
   ․프로세스 p의 task_struct에는 이러한 관계를 나타내는 다음과 같은 포인터 변수가 있다.
    *p_opptr(원래 부모) : p를 만든 부모 프로세스의 task_struct를 가리키고, 부모 프로세스가 종료되면 프로세스 1의 task_struct를 가리킨다. 따라서 쉘 사용자가 백그라운드 프로세스(background process)를 시작하고서 쉘을 빠져나가면 백그라운드 프로세스는 프로세스 1의 자식이 된다.  
    *p_pptr(부모) : p의 현재 부모(자식 프로세스가 종료하면 시그널을 받을 프로세스)를 가리킨다. 이 값은 대개 p_opprtr과 일치하지만 ptrace() 시스템 콜을 호출하여 p를 모니터 하게 해 달라고 요청한 경우처럼 다를 수도 있다.  
    *p_cptr(자식) : p가 가장 최근에 만든 프로세스의 task_struct를 가리킨다.  
    *p_ysptr(아우) : p의 현재 부모가 p를 만들고 바로다음에 만든 프로세스의 task_struct를 가리킨다.  
    *p_osptr(형) : p의 현재 부모가 p를 만들기 바로 전에 만든 프로세스의 task_struct를 가리킨다.  

 

(3) 프로세서 관계
   ․각각의 포인터로 표현한 프로세스 관계는 다음과 같다.

 


(4) 리눅스 커널에서의 프로세스 관계
   ․리눅스 커널에 존재하는 모든 프로세스들은 프로세스 1의 task_struct에서 시작하는 이중 연결 리스트로 연결되어 있는데, 이때 이용하는 포인터가 next_task, prev_task이다. 그리고 이 연결 리스트의 시작은 init_task라는 변수가 가리키고 있다.  
   ․모든 태스크들 중에 TASK_RUNNING 상태인 태스크들은 따로 이중 연결 리스트로 연결되어 있는데, 이때 이용하는 포인터가 next_run, prev_run이다. 이 연결 리스트의 시작은 run_queue라는 변수가 가리키고 있다.  

 

4) Thread structure
(1) thread structure의 개념
   ․thread structure는 프로세스가 실행하다가 중지할 때 프로세스가 현재 어디까지 실행했는지 기억하는 공간으로 CPU에 있는 레지스터들의 저장 값과 같은 내용을 가지고 있다. 따라서 스레드를 하드웨어 레지스터 문맥이라 부르기도 한다. 
   ․인텔 CPU의 경우 task_struct에 존재하는 변수인 thread의 자료형 thread_struct를 나타낸 것으로, 레지스터 값들이 저장될 수 있는 변수들로 구성되어 있다. 


  
thread structure의 필요성
▶프로세스는 실행 중에 다양한 상태 전이를 겪는다. 실행 중에 사건을 기다릴 필요가 있으면 대기 상태(TASK_INTERRUPTIBLE 또는 TASK_UNINTERRUPTIBLE)로 전이하고, 시간 할당량이 지나면 준비 상태(TASK_RUNNING에서 실행되기를 기다리는 상태)로 전이한다.

▶전이가 발생할 경우 프로세스는 어디까지 실행했는지 저장해 놓아야 한다. 그래야만 이후 다시 스케줄링 되어 실행될 때 중지한 다음부터 실행할 수 있기 때문이다. 또한 실행 중에 인터럽트가 발생할 때에도 프로세스가 어디까지 실행했는지 저장해 놓아야 한다. 그래야만 인터럽트 처리가 끝난 후 중지한 다음부터 다시 실행할 수 있기 때문이다. 

 

(2) Thread structure의 필요성
   ․프로세스는 실행 중에 다양한 상태 전이를 겪는다. 이때 프로세스가 어디까지 실행했는지 저장해 놓아야 한다. 그래야만 이후 다시 스케줄링되어 실행될 때 중지한 다음부터 실행할 수 있기 때문이다. 
   ․실행 중에 인터럽트가 발생할 때에도 프로세스가 어디까지 실행했는지 저장해 놓아야 한다. 그래야만 인터럽트 처리가 끝난 후 중지한 다음부터 다시 실행할 수 있기 때문이다.
   ․프로세스 실행에 대한 정보는 처리기의 pc(program counter, 인텔처리기에서는 eip 레지스터라고 불린다.) 레지스터에 저장된다. 그리고 프로세스는 수행 중에 커널 모드 프로세스 스택을 빈번하게 사용하며, 따라서 중지할 때 현재 스택의 사용 위치(top)가 어디인지 알아야 한다. 
   ․이 정보는 CPU의 sp(stack pointer) 레지스터를 이용해 알 수 있다. 뿐만 아니라 프로세스가 실행 중에 이용한 처리기의 범용 레지스터의 값들도 기억해 두어야 한다. 이는 프로세스가 중지되면 다른 프로세스가 처리기를 사용할 것이며, 따라서 다른 프로세스가 CPU의 레지스터 내용을 변경할 수 있기 때문이다. 
   ․이외에도 실행 중인 명령어의 위치를 가리키는 eip, 명령 실행 중 CPU 상태를 나타내는 eflags, 실행 중인 프로세스의 페이지 디렉토리를 가리키는 cr3, 전역 디스크립터 테이블(GDT : Global Descriptor Table)을 가리키는 gdtr, 프로세스별로 존재하는 지역 디스크립터 테이블(LDT : Local Descriptor Table)을 가리키는 ldtr, IDT를 가리키는 idtr 등의 레지스터들과 세그먼트를 가리키는 cs, ds, es, ss 등의 세그먼트 레지스터가 있다.

 

5) time information
   ․커널은 프로세스의 생성시간과 살아 있는 동안 소비하는 CPU 시간 등을 계속 추적한다. 
   ․커널은 매 클락 순간(tick)마다 현재 프로세스가 시스템 모드와 사용자 모드에서 사용한 시간의 양을 jiffies 단위로 갱신한다. 
   ․리눅스는 또한 간격 타이머(interval timer)도 지원하는데, 프로세스는 시스템 콜을 사용하여 타이머를 설정하고 지정한 시간이 지나면 자신에게 시그널을 보낼 수 있도록 한다. 이 타이머는 한번만 발생하는(single-shot) 타이머일 수도, 주기적으로 발생하는 타이머일 수도 있다. 
   ․task_struct에서 프로세스의 시간 정보를 위한 변수로는 start_time, times, real_timer 등이 있다. start_time은 프로세스가 생성된 시간이며, times는 사용자 수준과 커널 수준에서 프로세스가 실행한 시간이 기록된다. 그리고 real_timer는 지연된 루틴을 위한 변수이다.

6) resource limits
   ․리눅스는 각 프로세스마다 프로세스가 사용할 수 있는 자원의 양을 제한한다. 이런 제한을 통해 사용자가 자원(CPU와 디스크 공간 등)을 과도하게 사용하는 것을 막을 수 있기 때문이다. 
   ․리눅스에서 자원의 제한에 대해 도식화하면 다음과 같다.


   ․위의 그림에서 rlim_max는 최대 허용 자원의 수, 그리고 rlim_cur은 현재 설정된 허용 자원의 수를 의미한다. 자원의 한계는 배열로 구현되어 있으며, Linux 커널 2.4에는 다음과 같은 11개의 자원에 대한(RLIM_NLINIT=11) 한계를 설정할 수 있다. 

 

   ※ Linux 커널 2.4에서 자원에 대한(RLIM_NLINIT=11) 한계 설정
① RLIMIT_CPU(0 : CPU time in ms) : 프로세스가 사용할 수 있는 최대 CPU 시간. 프로세스가 이 제한 시간을 넘어 동작하면 커널을 SIGXCPU 시그널을 보내고, 그래도 프로세스가 종료하지 않으면 SIGKILL 시그널을 보낸다. 
② RLIMIT_FSIZE(1 : Maximum filesize) : 사용할 수 있는 최대 파일 크기(바이트 단위). 프로세스가 이 값보다 큰 파일을 만들려고 하면 커널은 SIGXFSZ 시그널을 보낸다. 
③ RLIMIT_DATA(2 : max data size) : 최대 힙 크기(바이트 단위). 커널은 프로세스의 힙 크기를 늘리기 전에 이 값을 검사한다. 
④ RLIMIT_STACK(3 : max stack size) : 최대 스택 크기(바이트 단위). 커널은 프로세스의 사용자 모드 스택 크기를 늘리기 전에 이 값을 검사한다. 
⑤ RLIMIT_CORE(4 : max core file size) : 최대 코어 덤프(core dump) 파일 크기(바이트 단위). 커널은 프로세스가 어떤 문제로 죽을 때 프로세스의 현재 디렉토리에 core 파일을 만들기 전에 이 값을 검사한다. 제한 값이 0이라면 커널은 이 파일을 만들지 않는다. 
⑥ RLIMIT_RSS(5 : max resident set size) : 프로세스가 소유할 수 있는 최대 페이지 프레임 수. 커널은 프로세스가 malloc()이나 자신의 주소 공간을 늘리는 관련 함수를 호출할 때 이 값을 검사한다. 
⑦ RLIMIT_NPROC(6 : max number of processes) : 사용자가 소유할 수 있는 최대 프로세스의 개수. 
⑧ RLIMIT_NOFILE(7 : max number of open files) : 최대로 열 수 있는 파일 디스크립터의 수. 커널은 새로 파일을 열거나 파일 디스크립터를 복사할 때 이 값을 검사한다. 
⑨ RLIMIT_MEMLOCK(8 : max locked-in-memory address space) : 스왑할 수 없는 메모리의 최대 크기(바이트 단위). 커널은 프로세스가 mlock()이나 mlockall() 시스템 콜을 이용하여 메모리에서 페이지 프레임을 락(lock)하려고 할 때 이 값을 검사한다. 
⑩ RLIMIT_AS(9 : address space limit) : 프로세스 주소 공간의 최대 크기(바이트 단위). 커널은 프로세스가 malloc()이나 자신의 주소공간을 늘리는 관련 함수를 호출할 때 이 값을 검사한다. 
⑪ RLIMIT_LOCKS(10 : maximum file locks held) : 최대 파일 락 개수. 커널은 프로세스가 파일에 락을 걸려고 할 때 이 값을 검사한다. 

 

7) 기타
   ․scheduling information : task_struct에서 스케줄링과 관련된 변수는 policy,? counter, rt_priority, 그리고 need_resched 필드이다. 스케줄링에 대해서는 12주차 강의에서 보다 자세히 설명한다. 
   ․signal information : 시그널은 프로세스에게 비동기적인 사건의 발생을 알리는 메커니즘이다. task_struct에서 시그널과 관련된 변수에는 signal_struct, sigpending, signal, 그리고 blocked 등이 있다. 이에 대해서는 12주차 강의에서 보다 자세히 설명한다. 

 

   ․memory information : 프로세스는 자신의 명령어와 데이터를 텍스트, 데이터, 스택, 그리고 힙 공간 등에 저장한다. tast_struct에는 이 공간에 대한 위치와 크기, 접근 제어 정보 등을 관리하는 변수들이 존재한다. 또한 가상 주소를 물리 주소로 변환하기 위한 페이지 디렉토리와 페이지 테이블 등의 주소 변환 정보들도 task_struct에 존재한다. 이러한 정보들은 task_struct에서 mm_struct라는 이름의 변수로 접근할 수 있다. 이에 대해서는 13주차 강의에서 보다 자세히 설명한다. 
 

  ․file information : 프로세스가 오픈한 파일들은 task_struct에서 file_struct라는 이름의 변수로 접근할 수 있다. 그리고 루트 inode와 현재 디렉토리의 inode는 fs_struct라는 변수로 접근할 수 있다. 이에 대해서는 14주차 강의에서 보다 자세히 설명한다. 

 

   ․format : 리눅스는 Linux exec 도메인뿐만 아니라 BSD나 SVR4 exec 도메인도 지원한다. 즉 BSD나 SVR4 커널에서 컴파일된 프로그램도 리눅스에서 재컴파일 없이 수행될 수 있다는 의미이다. 이를 위해 personality와 struct exec_domain 등의 변수가 사용된다. 그리고 다양한 이진 포맷(binary format)을 지원하기 위해 binfmt 변수가 사용된다. 현재 지원되는 대표적인 이진 포맷으로는 a.out 포맷, elf 포맷, java 포맷, 쉘 스크립트 포맷 등이 있다. 
   ․이 외에 프로세스 구조에는 프로세스의 flag 변수, 수행한 프로그램 이름을 나타내는 comm 문자열 변수, 수행 중에 발생한 페이지 결함 수를 의미하는 maj_flt와 min_flt 변수, 종료 이유를 나타내는 exit_code 변수 등이 있다. 

 

3. 프로세스이 생성, 전이, 종료
   ․Task_struct 자료구조는 각 프로세스마다 존재하며 실행과 관련된 모든 정보를 포함하게 되며, 새로운 프로세스가 생성될 때마다 하나씩 할당되게 된다. 프로세스는 생성되어 소멸될 때까지 다양한 상태 전이를 거치게 된다.프로세스도 언젠가는 종료되어야만 한다. 프로세스가 종료될 때 커널은 프로세스가 갖고 있던 자원을 반납시키고 부모 프로세스에게 이를 알려야 한다.

1) 프로세스의 생성
(1) fork() 함수
   ․fork()는 현재 프로세스를 복제하여 자식 프로세스를 생성한다. 
   ․생성된 자식은 부모 프로세스와 PID, PPID(부모의 PID, 원래 프로세스로 설정됨), 그리고 자원이나 지연된 시그널과 같은 통계수치에서만 차이가 난다. exec()는 주소공간에 새 실행파일을 로드하여 시작시킨다. 

 

(2) 리눅스에서 fork() 함수
   ․전통적인 fork()는 부모의 모든 자원을 복제하여 자식 프로세스로 넘겨주는데, 이 방법은 너무 단순하여 비효율적이다. 
   ․리눅스에서는 copy-on-write 기법을 이용하여 fork()를 구현한다. 
*copy-on-write(혹은 COW)란 데이터의 복제를 지연시키는 기법으로 자원의 복제는 쓰기가 발생할 때만 일어나게 된다. 
*생성 즉시 프로세스 주소 공간을 복제하는 것이 아니라 부모와 자식 프로세스가 하나의 공간을 공유하고, 이후 데이터를 써넣을 일이 발생하면 주소공간을 복제 하여 자식 프로세스에게 넘겨주는 것이다. 
*fork() 바로 다음에 exec()가 따라오는 경우와 같이 페이지에 쓰기가 전혀 발생하지 않는 경우에는 복제가 일어나지 않는다. 
*fork()에서 일어나는 것은 부모 프로세스의 페이지 테이블을 복제하고 자식 프로세 스를 위한 유일한 프로세스 서술자를 만드는 작업뿐이다.  
   *리눅스의 fork()는 clone() 시스템 콜을 이용하여 구현된다. 이 함수는 또한 어떤 자원을 부모와 자식 프로세스가 공유할 것인가를 지정하는 여러 플래그를 사용한다. 라이브러리 함수인 fork(), vfo가(), _clone()은 적절한 플래그를 사용하여 clone()을 호출한다. clone() 시스템 콜은 차례로 do_fork()를 호출한다. 

 

(3) 프로세스 생성 1단계
   ․프로세스 생성의 대부분의 작업은 do_fork()에서 처리되는데, 이것은 kernel/fork.c에 정의되어 있다. 이 함수는 copy_process()를 호출한 다음 프로세스를 시작시킨다. 
   ․copy_process() 함수의 동작은 다음과 같다. 
① dup_task_struct()를 호출하여 새 커널 스택과 thread_info 구조체, 그리고 새 프로세스를 위하여 현재 프로세스와 동일한 값들을 갖는 task_struct 구조 체를 생성한다. 이 시점에서 자식과 부모 프로세스 서술자는 완전히 동일하다.  
② 새 자식 프로세스가 현재 사용자의 프로세스 개수 자원 한계를 넘지 않는가를 검사한다.  
③ 이제 자식 프로세스를 부모와 구별할 필요가 있으므로, 프로세스 디스크립터 의 많은 멤버를 초기값으로 설정한다.  
④ 다음으로 자식 프로세스의 상태를 TASK_UNINTERRUPTIBLE로 설정하여 아직 실행되지 않도록 만든다.  
⑤ copy_flags()를 호출하여 task_struct 구조체의 flags 멤버를 갱신한다. 이 때 프로세스가 슈퍼 유저 권한으로 실행되는가를 나타내는 PF_SUPERPRIV 플래 그가 초기화되며, 아직 exec()를 호출하지 않은 프로세스를 나타내는 PF_FORKNOEXEC 플래그가 설정된다.  
⑥ get_pid()를 호출하여 새 프로세스에 가용한 PID를 부여한다.  
⑦ clone() 함수에 넘겨진 플래그에 따라 열린 파일, 파일시스템 정보, 시그널 핸들러, 프로세스 주소 영역, 네임스페이스(namespace) 등을 복제하거나 공유 한다.  
⑧ 남은 타임 슬라이스(time slice)를 부모와 자식 프로세스간에 분배한다.  
⑨ 마지막으로 새 자식 프로세스의 포인터를 반환한다.  

 

(4) 프로세스 생성 2단계
   ․만약 copy_process()가 성공적으로 반환되면, 새 자식 프로세스가 깨어나서 실행된다. 
   ․커널은 의도적으로 자식 프로세스를 먼저 실행시킨다. 
   ․대부분의 자식 프로세스는 바로 exec()을 실행할 것이므로, 이렇게 하는 것이 copy-on-write의 발생을 줄일 수 있다. 왜냐하면 부모 프로세스가 먼저 실행될 경우 주소 공간에 쓰기를 할 수 있고, 그렇게 되면 copy-on-write가 발생하기 때문이다. 

 

2) 프로세스이 전이

(1) 프로세스 전이의 과정
   ․프로세스의 상태 전이 과정을 도식화하면 다음과 같다.


(2) 프로세스 전이의 유형
   ․실행 상태에 있는 프로세스들은 발생하는 사건에 따라 다음과 같은 상태로 전이할 수 있다. 
   ․프로세스가 자신이 해야 할 일을 다 끝내고 exit()를 호출하면(혹은 kill되는 경우) 좀비 상태(zombie state)로 전이한다. 
*좀비 상태는 말 그대로 죽어있는 상태이며, 단지 자신이 소멸된 이유(error 번호), 자신이 사용한 자원의 통계 정보 등을 유지하고 있는 상태이다.  
*언젠가 부모 프로세스가 wait() 등의 방법을 통해서 이 정보들을 수집하면, 좀비 상태도 없어지면서 그 프로세스는 완전히 시스템에서 없어지게 된다.  
*부모 프로세스가 자식 프로세스보다 먼저 죽는다면 자식 프로세스의 부모 프로세스 는 init 프로세스로 바뀐다. 그리고 init 프로세스가 좀비 상태를 없애는 일을 대신 수 행하게 된다.  
   ․실행 상태에 있던 프로세스가 타임아웃(timeout)되어 다시 준비 상태로 전이할 수도 있다. 
*Linux 시스템은 여러 프로세스들에게 공평한 CPU 사용 시간을 할당하기 위해 시분할(time sharing) 방식을 사용한다.  
*이 방식은 각 프로세스를 실행시킬 때 일정한 시간 할당량(time quantum)만큼만 실행하도록 하고, 그 시간 할당량이 지나면 실행 중이던 프로세스와 준비 상태에 있던 프로세스들간에 우선 순위를 비교하여 가장 우선 순위가 높은 프로세스를 실행 시킨다.  
*결국 실행 상태에 있던 프로세스는 자신에게 할당된 시간 할당량이 지나면 준비 상태로 전이할 수 있으며, 이때 이 전이를 타임아웃이라고 한다.  
   ․실행 상태에 있던 프로세스가 특정한 사건을 기다려야 할 필요가 있으면 대기 상태(waiting state 또는 sleep state)로 전이한다. 
    대기 상태는 TASK_INTERRUPTIBLE 혹은 TASK_UNINTERRUPTIBLE로 어떤 이벤트의 발생을 기다리며 대기큐에서 휴면상태로 존재하며 실행 불가능하다. TASK_INTERRUPTIBLE는 시그널(signal)이 발생할 경우 휴면에서 깨어나 시그널에 응답하지만 TASK_UNINTERRUPTIBLE는 시그널을 무시하고 응답하지 않는다. 

 

(1) 결함 허용
   ․휴면은 대기큐를 통하여 처리된다. 
   ․대기큐는 이벤트의 발생을 기다리는 프로세스들의 단순한 리스트이다. 대기큐는 커널에서 wake_queue_head_t로 표현된다. 대기큐DECLARE_WAIT_QUEUE_HEAD()를 통하여 정적으로 생성되거나, nit_waitqueue_head()를 통하여 동적으로 생성된다. 
   ․프로세스들은 자신을 대기큐에 삽입하고 실행가능하지 않다고 표시한다. 
   ․만약 대기큐와 연관된 이벤트가 발생하면, 대기큐에 있는 프로세스들이 깨어난다. 경쟁 상태(race condition)를 예방하기 위해서는 이러한 휴면과 깨어남을 정확히 구현 하는 것이 중요하다.   

 

(2) 병렬처리를 통한 해결  
   ․깨어남(waking)은 wake_up()으로 처리되는데, 이 함수는 주어진 대기큐에 있는 모든 프로세스를 깨운다. 
   ․이것은 try_to_wake_up()을 호출하는데, 이 함수는 프로세스의 상태를 TASK_RUNNING으로 설정하고, activate_task()를 호출하여 프로세스를 실행큐에 삽입한다.
   ․만약 깨어난 프로세스의 우선 순위가 현재 프로세스의 우선 순위보다 높다면 need_resched를 설정한다. 대개의 경우 이벤트를 발생시킨 코드가 후에 wake_up()을 호출하게 된다. 

 

   ※ 휴면과 깨어남의 도식화


    
(2) 트리높이 단축의 유형
   ․프로세스의 실행 상태는 다시 사용자 수준 실행(user level running) 상태와 커널 수준 실행(kernel level running) 상태로 구분할 수 있다.
   ․다음 그림은 이것을 도식화한 것이다. 


   ․사용자 수준 실행 상태는 프로세스가 프로그래머가 작성한 프로그램이나 라이브러리 함수를 수행하는 상태로, 사용자 수준 권한으로 동작한다.
   ․반면에 커널 수준 실행 상태는 프로세스가 커널 프로그램의 일부분이 수행하는 상태로, 사용자 수준 권한보다는 더 강력한 커널 수준 권한으로 동작한다. 
   ․커널 수준 권한이 사용자 수준 권한보다 더 강력하다는 것은 사용자 수준 권한에서는 접근이 금지된 커널 내부 자료 구조를 접근하거나 수행이 금지된 특권 명령어를 커널 수준 권한에서 수행할 수 있다는 의미이다. 만일 프로세스가 사용자 수준 권한으로 커널 공간을 접근하려 하거나 특권 명령어들을 수행하려고 한다면, 보호 결함(protection fault)이 발생하게 된다.
   ․사용자 수준 실행 상태에서 커널 수준 실행 상태로 전이할 수 있는 방법 

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



    ․한편 커널이 시스템 호출의 서비스를 완료하거나 인터럽트 처리를 완료하면 커널 수준 실행 상태에서 사용자 수준 실행 상태로 전이한다. 이때 리눅스 커널은 몇 가지 매우 중요한 일들을 처리한다. 
 첫째, 커널은 현재 실행중인 프로세스가 시그널을 받았는지 확인하며 받았을 경우 시그널 처리 핸들러를 호출한다.  
 둘째, 다시 스케줄링 할 필요가 있으면(예를 들어 tast_struct의 need_resched 변수가 1로 설정되어 있는 경우) 스케줄러를 호출한다.  
 셋째, 커널 내에서 연기된(delayed) 루틴들이 존재하면 이들을 수행한다. 
    
(1) exit() 시스템 콜을 호출  
   ․일반적으로 프로세스는 exit() 시스템 콜을 호출함으로써 종료된다. 
   ․필요시 이 시스템 콜을 직접 호출할 수도 있고, 아니면 주 서브루틴이 리턴될 때 암묵적으로 호출되기도 한다. 
   ․프로세스는 비자발적으로 종료되기도 하는데 프로세스가 처리할 수 없거나, 무시할 수 없는 시그널을 받거나, 예외상황이 발생했을 경우가 그러하다. 

 

(2) do_exit() 시스템 콜을 호출  
   ․프로세스가 어떻게 종료되든 간에 많은 작업이 do_exit()에 의해 이루어진다. 
   ․do_exit()는 kernel/exit.c에 정의되어 있으며, 동작은 다음과 같다. 
① task_struct 구조체의 flags 멤버에 PF_EXITING 플래그를 설정한다.  
② 만약 BSD 프로세스 어카운팅(accounting)이 실행중이면 acct_process()를 호출하여 어카운팅 정보를 기록한다.  
③ _exit_mm()을 호출하여 프로세스가 잡고 있는 mm_struct 구조체를 반환시킨다. 만약 다른 프로세스에서 이것을 사용하고 있는 경우가 없다면(공유되고 있지 않다면) 그 메모리를 제거(deallocate)한다.  
④ sem_exit()를 호출한다. 만약 프로세스가 IPC 세마포어를 얻기 위해 큐에서 대기 중이었다면 이 시점에서 빠져 나오게 된다.  
⑤ _exit_files, _exit_fs, exit_namespace(), exit_sighand()를 호출하여 각각 파일 서술자, 파일시스템 데이터, 프로세스 네임스페이스, 시그널 핸들러와 관련된 객체의 사용 카운트를 감소시킨다. 만약 사용 카운트가 0이면, 그 객체는 어느 누구에 의해서도 사용되지 않으므로 제거한다.  
⑥ 프로세스의 종료 코드를 task_struct 구조체의 exit_code 멤버에 설정한다. 이 값은 exit()로부터 주어진 값이거나 혹은 커널의 종료 메커니즘에서 결정된 값이다.  
⑦ exit_notify()를 호출하여 현재 프로세스의 부모 프로세스에게 시그널을 보내고, 종료되는 프로세스의 자식 프로세스들의 부모 프로세스를 같은 스레드 그룹의 다른 스레드나 혹은 init 프로세스로 지정한다. 그런 다음 프로세스의 상태를 TASK_ZOMBIE로 설정한다.  
⑧ 마지막으로 schedule() 함수를 호출하여 새 프로세스로 교환(switching)한다. TASK_ZOMBIE 상태인 프로세스는 스케줄링에서 제외되므로 이것이 바로 종료되는 프로세스의 마지막 실행 코드가 된다.  

 

(3) 프로세스 종료 시점의 특징 
   ․do_exit() 함수의 실행시점에서 프로세스와 관련되었던 모든 객체는 반납된다. 
   ․이 프로세스는 더 이상 실행가능하지 않으며, TASK_ZOMBIE 상태에 놓이게 된다. 
   ․종료된 프로세스가 갖고 있는 메모리는 오직 커널 스택과 슬랩 객체들인데, 여기에는 각각 thread_info와 task_struct 구조체가 포함된다. 프로세스는 오직 부모 프로세스에게 정보를 전달하기 위해서만 존재한다. 
   ․부모 프로세스가 자식 프로세스에 대한 정보를 얻고 나면 자식 프로세스의 task_struct를 제거할 수 있다. 

 

(4) wait() 계열 함수 
   ․wait() 계열 함수들은 wait4() 시스템 콜 하나를 사용하여 구현된다. 
   ․d이 함수의 주된 역할은 함수를 호출한 프로세스를 자식 프로세스가 종료될 때까지 정지시키는 것인데, 이 함수는 리턴될 때 종료된 자식 프로세스의 PID를 반환한다. 또한 자식 프로세스의 종료 코드를 담는 포인터를 제공한다. 
    프로세스 서술자를 완전히 제거할 시점이 되면 커널은 release_task()를 호출한다. 이 함수는 다음의 과정을 통해 좀비 프로세스의 프로세스 디스크립터를 해제한다. 
① 종료한 프로세스의 소유자인 사용자가 지금까지 만든 프로세스의 수를 하나 줄인다.  
② free_uid() 함수를 호출하여 user_struct 구조체의 자원 참조 횟수를 1 줄인다.  
③ unhash_process()를 호출하여 nr_threads 변수 값을 1 줄이고, unhash_pid() 함수를 호출해서 pidhash 해시 테이블에서 프로세스 디스크립터를 제거한 뒤, REMOVE_LINKS 매크로를 사용하여 프로세스 리스트에서도 제거한다. 만약 스레드 그룹에 속해 있다면 프로세스를 이 그룹에서도 제거한다.  
④ free_task_struct()을 호출하여 프로세스 디스크립터와 커널 모드 스택으로 사용한 8KB 메모리 영역을 해제한다.  
 

 

 

 



'정보과학 > 운영체제특론' 카테고리의 다른 글

메모리 관리 커널  (0) 2023.12.16
프로세서 관리 커널 2  (0) 2023.12.16
임베디드 시스템과 리눅스  (0) 2023.12.13
운영체제 보안  (1) 2023.12.11
병렬처리  (0) 2023.12.09