gwimong's blog Software Engineer

리눅스에서의 Process

2010-11-11

프로세스와 쓰레드의 차이

프로세스는 일반적으로 실행 상태에 있는 프로그램의 인스턴트라고 정의됩니다.
커널의 관점에서 보면 프로세스의 목적은 시스템의 자원을 할당받는 것이라고 할 수 있습니다.

이러한 프로세스는 부모프로세스와 자식프로세스로 나눌수 있는데, 부모와 자식은 프로그램 코드가 들어 있는 페이지는 공유를 하지만 데이터는 서로 별개의 복사본을 가집니다. 그렇기 때문에 부모프로세스에서 메모리의 데이터를 바꾼다 하여도 자식프로세스는 이를 알지 못하고 그 반대도 마찬가지입니다.

이러한 문제를 해결하기 위해 파이프와 같은 기술을 이용하여 자식프로세스와 부모프로세스를 연결하지만, 간단한 작업을 하는데에도 사용하기에는 프로세스는 너무 무겁습니다.

그렇기에 나온것이 경량프로세스인데, 경량프로세스는 부모프로세스와 자원을 공유하는 프로세스입니다. 이러한 경량 프로세스를 다른 말로 ‘쓰레드’라고 하며 리눅스에서는 프로세스라는 개념보다는 쓰레드라는 개념으로 사용합니다.


프로세스 상태

프로세스의 상태는 7가지로 나눌수 있습니다.

  • TASK_RUNNING
    프로세스가 CPU를 할당받아 실행중이거나 실행되기를 기다리는 중인 상태

  • TASK_INTERRUPTIBLE
    프로세스가 어떤 조건을 만족하기를 기다리면서 보류 중인 상태.

  • TASK_UNINTERRUPTIBLE
    위에 TASK_INTERRUPTIBLE와 비슷한 상태,
    하지만 TASK_INTERRUPTIBLE와는 다르게 잠들어있는 프로세스가 시그널을 받아도 깨어나지 않습니다. 보통 잘 사용되지 않지만, 프로세스가 특정한 이벤트가 발생하기를 기다리는 도중에 방해를 받으면 안되는 특수한 상황에서 유용합니다. (예를 들면 프로세스가 장치 파일을 열때 해당 장치 드라이버가 자신이 사용할 하드웨어를 검사하는 도중은 해당 하드웨어를 찾기전까지 활성화되서는 안됩니다. 이런 경우 유용할수 있습니다)

  • TASK_STOPPED
    프로세스의 실행이 중단된 상태, SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU와 같은 시그널을 받으면 해당 프로세스는 이러한 상태가 됩니다.

  • TASK_TRACED
    프로세스의 실행이 디버거에 의해 멈춘 상태입니다.

  • EXIT_ZOMBIE
    프로세스가 종료되었지만, 부모 프로세스가 wait4() 또는 waitpid()와 같은 시스템 콜을 호출하여 종료 프로세스의 정보를 반환하지 않은 경우입니다. 부모프로세스가 wait() 계열의 시스템 콜을 호출하기전까지는 종료되었지만 종료한 프로세스의 프로세스 디스크립터에 들어 있는 데이터를 부모 프로세스에서 필요로 할 수도 있기 때문에 해당 프로세스를 완전히 종료시켜서는 안됩니다.

  • EXIT_DEAD
    EXIT_ZOMBIE에서 부모 프로세스가 wait4() 또는 waitpid() 시스템 콜을 수행하여 시스템에서 제거되는 중인 상태. 이는 같은 프로세스에 대해 다른 스레드가 wait()콜을 수행함으로써 경쟁 조건을 피하기 위함입니다.


프로세스 생명주기

img

프로세스 디스크립터

리눅스는 자신의 주소 공간에 NR_TASKS(해당 커널이 다룰 수 있는 프로세스의 수) 크가 만큼의 배열인 task를 전역 정적 변수로 정하고 있고, 이 배열의 각 항복은 프로세스 디스크립터의 포인터들입니다. 프로세스 디스크립터는 해당 프로세스에 대한 정보를 가지고 있는 자료구조입니다. 리눅스는 8KB 메모리 영역 하나에 각 프로세스에 대해 프로세스 디스크립터와 커널 모드 프로세스 스택을 합께 저장을 합니다.

img

스택은 위에서부터 저장하여 아래로 자라나며, 프로세스 디스크립터는 아래에서부터 저장을 합니다. 이렇게 스택과 디스크립터를 함께 저장하는 이유는 cpu가 현재 실행 중인 프로세스의 디스크립터 포인터를 가져오려고 할때 커널은 esp 레지스터 값을 이용하여 쉽게 가져올 수 있습니다. 실제로 메모리 영역의 크기는 8KB이므로 커널은 esp 레지스터의 뒤의 13비트만 지우면 프로세스 디스크립터의 시작 주소를 얻을 수 있습니다.

movl    $0xffffe000, %ecx
andl    %esp, %ecx
movl    %ecx, p

이렇게 단 3가지 명령어로 p에 프로세스의 thread_info 자료구조 포인터를 가져 올 수 있습니다. 만약 프로세스 디스크립터의 주소를 가져오고 싶다면 current 매크로를 이용하면 되는데, 이는

movl    $0xffffe000, %ecx
andl    %esp, %ecx
movl    (%ecx), p

로 구성되어있습니다. 예를들어 current->pid 이런식으로 현재 실행 중인 프로세스의 프로세스ID를 구합니다.


Pidhash 테이블

커널은 pid를 가지고 프로세스 디스크립터 주소를 알아야 하는 경우가 있습니다.
이때 프로세스 리스트를 순차적으로 검색하여 프로세스 디스크립터의 pid 필드를 확인하는것은 매우 비효율적이기 때문에 4개의 해시 테이블을 이용합니다.

해시 테이블이 4개인 이유는 프로세스 디스크립터는 PID의 다른 유형들을 표시하는 필드들을 가지고 있기때문입니다.

img img

img

위에 그림에서는 TGID 해시 테이블이에서 70번째 필드에 있는 프로세스 디스크립터를 가져오는 것입니다.
TGID 필드는 pid_hash테이블의 두번째 필드 이므로 pids[1]의 값을 가져오게 되고, pids[1]에는 pid 번호과 4351인 프로세스 디스크립터를 가리키고 있습니다. pid가 4351인 스레드 그룹의 프로세스들이 pid_list으로 연결되어 있고, 만약 찾고자하는 프로세스 디스크립터의 nr이 4351이 아닌경우 pid_chain에 연결되어 있는 nr이 246인 프로세스 디스크립터로 찾아가면 됩니다.


프로세스 생성

최근 유닉스 커널은 세가지 방법으로 프로세스를 생성합니다.

‘Copy On Write’ : 부모와 자식이 모두 동일한 물리적 페이지를 읽을 수 있게 합니다. 둘 중 하나라도 페이지에 쓰려고 하면 커널은 프로세스에 새로운 물리적인 페이지를 할당하여 내용을 복사합니다.

경량 프로세스(Lightweight Process) : 페이지 테이블과 열린 파일 목록, 시그널 배열 등 프로세스에게 할당되는 많은 커널 자료 구조를 부모와 자식이 공유합니다.

vfork() : 부모의 메모리 주소 공간을 공유하는 프로세스입니다. 부모 프로세스가 자식이 필요로 하는 데이터를 덮어쓰지 않도록, 자식 프로세스가 종료하거나 새로운 프로그램을 실행하기 전까지 부모 프로세스는 수행을 잠시 중단합니다.


Comments

Content