[시스템 프로그래밍] 프로세스 (1)
프로그램이 실행된다는 건, 컴퓨터 내부의 회로들이 시간의 흐름에 따라 작동하고 있음을 의미한다.
즉, 한 번에 한 개의 명령어들이 반복적으로 실행되고, 제어 회로는 명령어가 제대로 실행되도록 돕는다.
하지만, 때로는 제어 흐름이 변경돼야 하는 경우도 있다.
한글 문서 작업을 하고 있는 도중 카카오톡 메세지가 도착했다고 생각하자.
컴퓨터는 한글 문서 작업 관련 명령어를 처리해야 되기 때문에 카카오톡 메세지의 도착을 무시해야 할까?
이런 컴퓨터는 잘 팔리지 않을 것이다.
컴퓨터는 위와 같은 돌발적인 사건에 대응할 수 있어야 한다.
이런 사건을 예외적인 제어 흐름이라고 부르고, 하드웨어와 소프트웨어 두 가지로 나뉜다.
1. 하드웨어
컴퓨터구조에서 배운 Interrupt를 생각하면 이해하기 쉽다.
시스템 이벤트에 대한 반응으로 제어흐름을 변경한다.
컴퓨터를 설계할 때 기본적인 회로에 추가로 특정 사건을 예외로 설정한다.
예를 들면 더하기 빼기를 일반 사건으로 설정하고, 키보드에서 문자가 입력되거나 와이파이가 연결되는 사건을 예외로 설정한 후 예외가 발생 시 논리회로의 타이밍 신호를 발생시키는 방식으로 동작한다.
예외로 설정된 사건이 발생하면 소프트웨어가 반응한다.
이 때 소프트웨어는 OS에 작성돼있고, OS가 원래 실행하던 명령어를 잠시 멈추고 예외를 처리한다.
이 작업은 매우 빠르게 처리돼 사용자가 알아챌 수 없다.
2. 소프트웨어
하드웨어보다 위에 위치한 매커니즘이다.
프로세스간 Context 전환, 시그널, nonlocal 점프 등이 소프트웨어에서 처리된다.
추후 자세히 알아보자.
즉, 예외 상황은 특정 이벤트에 대한 반응으로, OS (커널) 로 제어가 전환되는 작업을 의미한다. (일단은 OS가 커널을 같은 단어라고 생각하자)
유튜브 영상이나 문서 작업 (User Code) 을 하다가 예외 상황이 발생했다.
예외 상황은 커널에서 처리된다. 커널로 Instruction Pointer가 넘어간다.
커널에서 예외를 처리하고 User Code 작업을 재개한다.
발생한 예외에 따라서 작업을 재개하거나, 처음부터 시작하거나, 중단 할 수 있다.
예외가 Interrupt와 비슷하다고 말했는데, 사실 modern CPU에서는 사용하는 용어가 좀 다르다.
요즘의 CPU는 처리해야 하는 사건이 너무 많기 때문에 Interrupt로는 모든 예외를 설명할 수 없다.
modern CPU는 예외 처리를 위한 Exception Table을 가지고, 이 Table을 사용해 예외를 바로바로 처리한다.
(Exception Table은 다른 이름으로 사용되기도 한다)
각 이벤트 타입은 예외 번호 k를 가지고, 핸들러 k는 예외 k가 발생할 때 호출된다.
이런 작업을 하는 회로가 CPU 내부에 구현돼있다.
Interrupt는 언제 발생할지 알 수 없어 Asynchronous 하다고 표현하고, 그 외 예외들은 Synchronous 로 표현한다.
어떤 명령어의 실행과 연관돼 Synchronous 성질을 가진다.
Interrupt 관련 예시로 노트북의 전원 버튼을 길게 눌렀다고 생각해보자.
일정 시간 이상으로 버튼이 눌림을 감지한 후 CPU에 우선순위가 제일 높은 예외가 발생한다.
Exception Table을 참고해 핸들러를 찾은 후 핸들러를 실행시킨다.
소프트웨어 리셋은 작업 관리자를 여는 경우를 생각하면 된다.
ctrl + alt +del 버튼을 누르면 작업 관리자가 실행되고, 작업 관리자를 핸들러로 예외를 처리할 수 있다.
1. Traps
명령어의 결과로 발생하는 의도적인 예외를 의미한다.
system call / breakpoint trap / special instruction 등이 Traps에 속하고, 처리 후 다음 명령어로 복귀한다.
OS의 API를 사용할 수 있도록 하는 명령어이고, 모든 CPU는 Trap을 발생시키는 어셈블리 명령을 가진다.
2. Faults
핸들러가 고칠 수 있는 에러의 결과로 발생한다.
page fault / protection fault (segmentation fault) / floating point exception 등이 Faults에 속한다.
Fault를 발생시킨 명령어를 다시 실행하거나, Abort한다.
3. Aborts
하드웨어 오류 처럼 복구할 수 없는 에러의 결과로 발생한다.
응용 프로그램으로 복귀할 수 없고, 현재 프로그램을 종료시킨다.
Trap을 유발하는 system call은 고유의 번호를 가지고, 번호를 기준으로 호출한다.
fileOpen 함수를 사용한다고 해 보자.
함수 내부에서 syscall명령어를 호출하고, %rax 레지스터에 리턴값을 저장해 실행 결과를 저장한다.
OS 부분에 구현돼있기 때문에 syscall의 구현에 대해서는 신경 쓰지 않아도 된다.
High Level 에서는 OS가 메모리 접근을 관리한다.
modern CPU는 페이지 단위로 메모리에 접근할 수 있도록 매핑해주는 가지고 있고, OS를 가진 컴퓨터는 페이지 단위로 메모리에 접근해야 한다.
페이지에 메모리가 있구나.. 하고 메모리에 접근을 시도했는데 메모리가 없다면?
즉, OS가 제공하는 가상 메모리 시스템으로 메모리의 특정 페이지가 하드디스크에 위치한다면?
페이지 핸들러는 해당 페이지를 물리 메모리에 로드해야 한다.
이 때 페이지 오류가 발생하고, 오류를 처리할 수 있으면 처리한 후 명령어를 다시 실행한다.
(자세한 내용은 OS에서 배운다)
여기서 페이지 오류가 Fault 예외 중 하나이다.
프로그램이 실행 상태에 있을 때 프로세스라고 부른다. (Excutable file에 대한 Runtime Instance)
프로세스는 각 프로그램이 CPU를 혼자 사용하는 것 처럼 보이도록 하고, 주 메모리를 독점하는 것 처럼 보이도록 한다. (추상화)
이런 추상화는 interleaving / multitasking / 가상 메모리 시스템 으로 구현된다.
Multi-Processing
예전에는 단일 프로세서가 다수의 프로세스들을 사용자가 눈치채지 못할 정도의 속도로 교차해서 하나씩 실행하는 방식으로 멀티프로세싱을 구현했다.
즉, 한 순간에는 하나의 프로세스만 실행되지만 속도가 너무 빨라 동시에 실행되는 것 처럼 인식된다.
요즘은 코어 여러 개를 한 번에 사용한다.
따라서 코어 개수에 따라 병렬로 동작하는데...
하지만 프로세서가 너무 많이 로딩되어있기 때문에 switching 작업이 발생하기는 한다.
메모리는 하나로 공유되고, CPU가 여러 개 있다고 생각하면 된다.
두 프로세스들에 대해 실행 시간이 중첩된다면 동시에 실행된다고 부르고, 실행 시간이 중첩되지 않는다면 순차적으로 실행된다고 부른다. ({A, B} 는 동시실행 / {B, C}는 순차실행)
프로세스들간의 context switching 작업이 매우 빠른 속도로 이루어지기 때문에 위와 같이 끊어지지 않은 채로 연결되는 것 처럼 보이기도 한다.
이제는 뒷편에 OS (커널) 가 프로세스들을 관리하며 context switching을 수행하고 있음을 알고있다.
(커널 코드는 어셈블리로 작성된다)
이제 프로세스를 제어 해 보자.
pid_t getpid(void) : 프로세스의 id를 가져올 때 사용한다.
pid_t getppid(void) : 프로세스 부모의 id를 가져올 때 사용한다.
int fork(void) : 호출하는 프로세스와 동일한 새 프로세스를 생성한다. (호출하는 프로세스는 부모, 생성된 프로세스는 자식)
자식 프로세스와 부모 프로세스는 concurrent하게 동작하기 때문에 어떤 프로세스가 더 빨리 실행되는지는 알 수 없다.
부모와 자식은 동일한 상태로 시작하고, 각각의 사본을 가진다.
fork() 함수의 리턴값이 0이면 자식을 의미하고, 0이 아니면 부모를 의미한다.
OS 영역으로 들어가 자신의 context를 메모리에 하나 복제한다.
fork() 함수가 포함된 코드를 정확하게 해석하는게 매우 중요하다.
프로세스 그래프를 그려서 생각하면 흐름을 파악할 때 도움이 된다.
스케쥴러는 그냥 ready 상태인 프로세스들의 리스트를 보고 그때 그때 어떤 프로세스를 먼저 실행시키기 정하기 때문에, 부모 프로세스와 자식 프로세스의 동작 순서는 무작위로 정해진다.
void exit(int status) : 종료 상태 값을 가지고 종료한다. (0은 정상을 의미)
exit로 프로세스를 종료해도 table에 데이터가 남아있게 된다.
프로세스가 종료됐지만 여전히 시스템 자원을 점유하는 프로세스를 좀비 프로세스라고 부르고, 모든 프로세스는 종료 시 좀비 프로세스가 된다.
시스템에 좀비가 많으면 시간이 지날수록 메모리가 줄어든다.
부모는 자식 프로세스가 좀비 프로세스가 되지 않도록 자식이 죽을 때 정리 해 줘야 하지만..
따로 코드로 작성하지 않아 부모가 정리하지 않는 경우 OS의 init 이라는 프로세스가 작동해 좀비 프로세스를 제거한다. (제거되는 부모도 정리된다)
부모가 종료됐지만 자식 프로세스가 여전히 살아있는 경우, kill 이나 다른 명령어로 해당 프로세스를 제거해야 한다.
init 프로세스는 부모가 죽을 경우만 실행된다. (init 프로세스 번호는 1번이다)
부모가 잘 죽지 않는 서버 프로그램 쪽에서는 좀비 제거에 주의해야 한다.
'Computer Science > System Programming' 카테고리의 다른 글
[시스템 프로그래밍] 시그널 (0) | 2022.11.14 |
---|---|
[시스템 프로그래밍] 프로세스 (2) (1) | 2022.11.06 |
[시스템 프로그래밍] 프로시저 (0) | 2022.10.28 |
[시스템 프로그래밍] 어셈블리어 - 반복문 (0) | 2022.10.24 |
[시스템 프로그래밍] 어셈블리어 - 조건문 (0) | 2022.10.23 |
댓글
이 글 공유하기
다른 글
-
[시스템 프로그래밍] 시그널
[시스템 프로그래밍] 시그널
2022.11.14 -
[시스템 프로그래밍] 프로세스 (2)
[시스템 프로그래밍] 프로세스 (2)
2022.11.06 -
[시스템 프로그래밍] 프로시저
[시스템 프로그래밍] 프로시저
2022.10.28 -
[시스템 프로그래밍] 어셈블리어 - 반복문
[시스템 프로그래밍] 어셈블리어 - 반복문
2022.10.24