2. 시스템 콜이 무엇인지 설명해 주세요.
시스템콜이란?
- 하드웨어를 직접 사용자가 다루는것은 시스템에 위험을 가져다 주기 때문에, 하드웨어는 os를 통해서 관리된다. 이 하드웨어를 관리하는 영역을 커널 영역이라고 하는데, 사용자가 커널 영역의 사용이 필요하기에, 이 사용자영역에서의 커널 영역에서의 동작을 수행하기 위해 정의된 인터페이스를 시스템 콜이라고 한다.
우리가 사용하는 시스템 콜의 예시를 들어주세요.
- printf(), fopen()등의 함수를 호출하는 것도 시스템 콜이다. 또한 우리는 CLI를 통해서도 시스템콜을 실행한다. 윈도우 처음 키고 나서, 구글링을 하기 위해 크롬을 키는것도 시스템 콜이다.
- 종류별로 조금 더 세분화하면, 다음과 같다.
- 프로세스 관리 측면: fork() execve(), exit(), wait(), waitpid() …
- 파일 I/O: open(), read(), write(), close(), lseek(), stat() / fstat() …
- 메모리 관리: mmap(), mummap(), brk() / sbrk(), mprotect()
- 파일 시스템 관리
- 디바이스 I/O
- 통신: socket(), bind(), listen(), accept() …
- c언어에서 쓸때 우리는 이걸 그대로 쓸 수 없다. 그래서 우리는 다른 함수를 호출하는데, 그 함수를 wrapper 함수(래퍼 함수)를 호출해서 내부 로직에서 시스템 콜을 호출하는 방식으로 쓴다. (pintos를 생각해보면 된다.)
시스템 콜이, 운영체제에서 어떤 과정으로 실행되는지 설명해 주세요.
c언어 기준으로 설명하겠다.
- 사용자 프로그램에서 시스템콜 요청
- c언어 내부에 read() 라는 wrapper 함수를 호출 (시스템콜 번호를 cpu 내부에 세팅)하여 syscall 특수 어셈블리 명령어를 실행
- 유저모드 → 커널모드 전환
- cpu의 현재 모드를 유저모드에서 커널모드로 전환
- 시스템 콜 번호를 보고, 시스템 콜 테이블에 대응되는 함수 실행
- Sys_read = 0 → sys_read() 실행
- 커널에서 시스템콜 처리
- 커널 내부의 sys_read() 함수를 호출해, 권한 및 파일디스립터등, 운영체제에서 시스템 콜을 호출했을때의 동작을 수행함
- 결과 반환 및 유저모드로 복귀
- 결과를 CPU 레지스터에 담고, CPU는 유저모드로 돌아가고 C 프로그램에 반환
시스템 콜의 유형에 대해 설명해 주세요.
- 우리가 사용하는 시스템 콜의 예시를 들어주세요. 이걸 보세용~
운영체제의 Dual Mode 에 대해 설명해 주세요.
- 운영체제는 2가지 모드를 제공한다. (유저모드와 커널모드) cpu가 명령어를 처리할 때, 비트를 전부 받으면서 cpu의 처리를 어떻게 할지 정하는데, 그 특정 비트를 supervisor mode bit라고 한다.
- Mode bit = 0 : Kernel Mode
- Mode bit = 1 : User Mode
- 컴퓨터를 처음 부팅할때는 CPU는 하드웨어를 초기화 하기 때문에, 커널모드로 시작합니다.
- 응용 프로그램을 실행할때, Mode Bit를 1로 변경하여 실행합니다.
- 시스템 콜 / 인터럽트 발생 시에 mode = 0으로 변경하고, 처리 완료되면 mode = 1로 변경해 사용자 모드로 돌아온다.
왜 유저모드와 커널모드를 구분해야 하나요?
여러 이유가 있다.
- 보안
- 하드웨어에 만약 사용자가 접근 가능하다면? 예를 들어서 메모리에 침범하면, 정상 작동을 하지 않게 됨
- 시스템 안정성
- 여러 프로세스를 관리할때, 사용자가 하나의 프로세스가 독점하게 만든다면? 다른 시스템은 전부다 의도치 않게 꺼지게 된다 (메모리조차도 다 침범하는 상황이 발생하기에)
- 자원 관리
- 각 프로세스별로 실행시간의 우선순위별로 실행하여 공정성을 부여한다.
- 인터페이스 단순화
- 하드웨어별로 다 다르기 때문에, 이걸 하나로 묶을 중간 단계가 필요하기 때문에, 추상화 측면에서 이점을 취할 수 있다.
서로 다른 시스템 콜을 어떻게 구분할 수 있을까요?
각 시스템콜은 각각의 고유번호를 가지고 있어서 번호로 구분하고, 리눅스 os (오픈소스)는 unistd.h에 헤더파일에 다 정의되어져 있다.
조금 더 자세히 얘기하면, cpu가 특수 레지스터에 시스템콜 번호를 저장하고, 다른 레지스터에 인자 값들을 저장하여 시스템콜을 실행한다
3. 인터럽트가 무엇인지 설명해주세요
CPU가 가지는 이벤트(통으로 인터럽트라고도 얘기함)에 종류가 4가지가 있다.
- Interrupt
- 하드웨어가 발생시키는 비동기적 신호 (키보드, 네트워크 패킷 도착)
여기서부턴, 전부다 Exception이라는 클래스로 정의되어진 하위 클래스들이라고 생각하자
- Trap
- 프로그램이 의도적/비의도적으로 커널을 호출 (시스템 콜)
- Fault
- 예외의 경우 divide by zero, page fault등등
- 추가로, 소프트웨어 인터럽트랑 다르다.
- 소프트웨어 인터럽트는, 트랩을 발생시키는 기계어 명령어, 다시말해 소프트웨어 인터럽트는 고의적 호출이고,
- Abort
이중에서 키보드 신호, 네트워크 패킷이 인터럽트에 해당한다.
인터럽트란, 기존 처리 중, 예외의 경우가 발생하거나 급한 사건이 발생하는 경우에 흐름을 바꾸는 작업니다. 더 쉽게 설명하자면, 예상하지 못한 사건에 CPU에게 처리를 다른 다른곳에 먼저 하도록 cpu 점유를 위임한다고 생각하면 된다.
하드웨어 인터럽트
소프트웨어 인터럽트
4. 프로세스가 무엇인가요?
프로그램과 프로세스, 스레드의 차이에 대해 설명해 주세요
- 프로세스는 프로그램의 실행 단위이고 스레드는 프로세스의 실행 흐름이라고 얘기한다. 프로그램도 정의를 할 줄 알아야 하는데, 프로그램은 디스크에 저장된 실팽 파일을 얘기하고, 명령어와 데이터만을 가지고 있기에, context가 없이 단순히 0과 1로만 이루어진 파일이다. 그리고 실행중인 상태가 아니기때문에, cpu 레지스터, 스택, 프로세스 같은 상태를 저장하는 PCB는 없는 상태이다.
- 프로세스는 프로그램(디스크에 저장된 실행 파일)이 메모리에 올라와 실행중인 인스턴스를 얘기하고, 이건 운영체제가 관리한다. (PCB(Process Control Block))으로 관리한다. 그리고, 프로세스마다 독립된 공간을 보유하고 있어서 프로세스끼리의 통신이 자체적으로 불가능하고, IPC를 통해서 프로세스끼리 통신한다. 프로세스는 다음과 같은 공간을 가지고 있다.
- Code 영역 ( 텍스트 ): 실행할 기계어 코드
- Data 영역: 전역 / 정적 변수
- Heap 영역: 동적 할당
- Stack 영역: 함수 호출 스택, 지역 변수, 멀티 스레드의 경우, 스레드도 존재
- 스레드는 프로세스 안에 존재하는 실행 단위이다. 프로세스에 위의 메모리 구조를 공유 하고있다. 그리고 스레드는 프로세스의 힙메모리에 상주하여 code/data/heap 영역을 공유할 수 있다. 스레드로 인해 병렬 / 동시성을 달성하여 프로그램을 빠르게 실행할 수 있다.
- 둘의 가장 큰 차이는 스레드는 스레드끼리 공유하는 자원이 존재하는 것이고, 프로세스는 각 프로세스끼리는 독립적이라는 것이 가장 큰 차이이다. 그렇기에 스레드는 공유자원 접근 (Race condition) 문제를 해결할 줄 알아야 한다.
PCB가 무엇인가요?
- PCB는 Process Control Block으로, 프로세스를 관리하기 위해 일관성있게 유지되는 하나의 구조이다. 프로세스의 태그와 같다고 생각하고, 각각 pid를 가져 프로세스를 식별하고, 내부에 프로세스가 어떤식인지 정보를 담고 있다. 또한 프로세스가 생성되면 커널 공간에 생성되고, 프로세스의 생명주기와 동일하다. PID, 프로세스 상태(running, ready, waiting 등), CPU레지스터 정보(PC, SP, 레지스터 값등), 메모리 관리 정보 등등이 저장된다.
그렇다면 스레드는 PCB를 가지고 있을까요?
- 스레드는 PCB를 가지고 있지 않지만, TCB라는 자료구조를 따로 두고있다. 다시 말해 스레드별 실행 정보를 관리할 수 있는 스레드ID, 레지스터 값, 스택 포인터, 상태등을 가지고 있는 것이고 결국 스레드는 프로세스 안에 있기 때문에, PCB 내부에 TCB를 가리키는 포인터들이 있거나 TCB테이블을 두고 PCB와 연결하는 방식을 채택한다. Unix / Linux : 스레드도 프로세스로 취급, 단순히 공유하는 여부로 tcb와 pcb를 구분한다 Window: TCB, PCB 를 구분하는 방식
- 추가로, TCB가 훨씬 가볍다. 메모리, 파일, IPC등의 정보를 저장하지 않고, 레지스터, PC, SP, 상태정도의 기본 정보만을 가지고 있기 떄문이다. 그래서 TCB에서 컨텍스트 스위치가 발생해도 비용이 적다.
리눅스에서 프로세스와 스레드는 각각 어떻게 생성될까요?
- 리눅스에서 프로세스가 생성되는것은 시스템콜인 fork()를 통해서 새로운 프로세스를 생성하게 된다. 그리고, 부모 프로세스를 반드시 상속 받기 때문에, PPID값으로 부모 프로세스를 가지고, 루트 프로세스는 커널이다. 그리고 fork()는 메모리만 생성하고 부모프로세스의 정보를 그대로 복사하고 현대에 들어서는 수정될 때 복사하는 방식을 쓴다. fork() → exec() 로 자식 프로세스를 새로 생성하여 실행하는 형식이다.
- 스레드의 경우는 clone()을 호출하여 실행단위를 새로 생성하고, 여러 플래그 값을 지정하는데 매개변수로 전달한다.
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
);
위에서 얘기했던 대로, 데이터, 힙, 코드 영역은 그대로 공유하는 방식이다.
자식 프로세스가 상태를 알리지 않고 죽거나, 부모 프로세스가 먼저 죽게 되면 어떻게 처리하나요?
- 자식프로세스가 죽으면 원래는 부모 프로세스에게
SIGCHLD 시그널을 보낸다. 부모가 wait() / waitpid()로 자식의 상태를 수거하면, 자식 PCB는 정리된다. 하지만 자식이 죽을때 수거하지 않으면 좀비 프로세스가 되기 때문에 PCB정보가 남아 있어서 PID를 차지하고 있는 상태가 된다. 일단 자식 프로세스를 수거하지 않으면 좀비 프로세스가 된다.
- 그리고, 부모프로세스가 먼저 죽으면, 자식 프로세스의 부모 프로세스를 init프로세스로 새로 지정하여, wait()호출을 통해서 자식 프로세스를 수거를 대신한다. 하지만 기본적으로 비정상 종료가 아니면, 부모가 wait()을 호출하고 부모가 자식을 정리한 후 종료한다.
리눅스에서 데몬프로세스에 대해 설명해주세요
- 데몬 프로세스는 백그라운드에서 실행되는 프로세스이다. 사용자와 직접적으로 연결되지 않고, 시스템 서비스를 제공한다. 데몬은 사용자 세션과 독립적으로, 백그라운드에서 특정 서비스를 제공하는 프로세스라고 생각하자.
리눅스는 프로세스가 일종의 트리를 형성하고 있습니다. 이 트리의 루트 노드에 위치하는 프로세스에 대해 설명해 주세요.
- 부모프로세스가 종료되었을때, 자식 프로세스가 어떻게 되는지 설명하는 단계에서 설명했던, pid=1인 프로세스 즉 sytemmd 또는 init 프로세스를 뜻하고, 모든 프로세스의 조상 프로세스가 된다. 또한 데몬 프로세스(시스템 서비스)를 를 관리한다.
- 부팅에 대해 얘기하면 조금 더 자세히 말할 수 있는데, BIOS/UEFI → 부트 로더 → 커널 로드 → 하드웨어 제어권 확보 까지의 과정에서는 사용자 영역이 아닌 커널 영역안에서 프로세스를 실행시킨다. 이제 커널이 초기화가 다 된다면, 유저공간을 생성하고, 유저공간에 첫 프로세스로 init/systemd가 실행된다.
5. 프로세스 주소공간에 대해 설명해 주세요.
초기화 하지 않은 변수들은 어디에 저장될까요?
일반적인 주소공간 그림처럼, Stack과 Heap의 크기는 매우 크다고 할 수 있을까요? 그렇지 않다면, 그 크기는 언제 결정될까요?
0xFFFFFFFFFFFFFFFF ────────────────────────────────────────────────
│ Kernel Space │ (Ring 0)
│ · 커널 코드/데이터 │
│ · 물리메모리 맵핑(Direct map) │
│ · 커널 스택(프로세스당), 슬랩/SLUB 캐시 │
│ · 드라이버/모듈, vmalloc 영역 등 │
├───────────────────────────────────────────────┤
│ (유저/커널 경계: 시스템콜 넘어감) │
├───────────────────────────────────────────────┤
0x00007FFFFFFFFFFF │ User Space │ (Ring 3)
↑ │ [스택 영역: per-thread stack] │ ← 위로 성장(grow down)
│ │ └─ guard page (스택 오버플로 보호) │
│ │ [vvar / vdso] │ (커널이 노출하는 읽기 전용 페이지들)
│ │ [mmap 영역] │ ← 공유 라이브러리, 파일 매핑, anon mmap
│ │ └─ JIT, 메모리 매핑 파일 등이 여기 │
│ ├───────────────────────────────────────────────┤
│ │ [Heap] │ ← 위로 성장(grow up, brk/sbrk, 일부는 mmap)
│ ├───────────────────────────────────────────────┤
│ │ [.bss] 초기화 안 된 전역/정적(런타임 0으로) │
│ │ [.data] 초기화 된 전역/정적 │
│ │ [.rodata] 상수 데이터 │
│ │ [.text] 실행 코드 │
│ ├───────────────────────────────────────────────┤
0x0000000000000000 │ NULL 근처 가드/미할당 구역 │
└───────────────────────────────────────────────┘
스택은 실행 시점 (프로세스 시작) 할때 제한 크기가 정해진다.
힙은 실행 도중에 커지기 때문에 런타임 중에 결정된다. (brk, sbrk 등으로 힙의 끝 포인터를 확장시킨다.)
Stack과 Heap 공간에 대해, 접근 속도가 더 빠른 공간은 어디일까요?
- Stack
- LIFO 구조로, 컴파일러가 스택 프레임 오프셋을 정해준다.
- 레지스터 기준 오프셋 연산이라서 cpu입장에서는 더하기만 해주기 때문에 연산 속도가 매우 빠르다.
- Heap
- 동적 할당 영역이고, 내부에 메모리가 free list(비어있는 공간을 리스트로) 관리 및 메타데이터 (프롤로크, 에필로그, 헤더, 푸터 등) 오버헤드가 존재한다.
- 하나의 malloc으로 할당받은 공간은 헤더와 푸터가 필요하고, 이걸 이용해서 다음 리스트의 크기를 찾는다.
- 참고로, 헤더와 푸터는 같은 비트로 구성되어져 있고, 마지막 3비트로 할당되어져 있는지 아닌지 판단한다.
- 패딩, 프롤로그, 에필로그, 등등 많은 메타데이터가 존재한다.
- 새로운 메모리 요청시에는 시스템콜을 호출하기에 더 느리다.
Stack의 접근은 offset 계산이고, Heap은 list로 관리되기 때문에 stack이 훨씬 빠르다.
다음과 같이 공간을 분할하는 이유가 있을까요?
- 역할 분리와 보안
- 코드 영역은 보통 Read-Only이고, 프로그램 실행 중 프로그램이 코드를 덮어쓰지 못하도록 막는다.
- 데이터/스택/힙은 쓰기는 가능하지만, 코드와 분리하여 보안 취약점을 줄인다
- 관리 편의성
- 전역 / 정적 변수 (Data, Bss) 는 프로그램 시작 시 운영체제가 한번 초기화 한다.
- 스택은 함수 호출, 리턴 구조와 맞아저 관리가 쉽고, 자동 해제
- 동적 할당에 적합한 영역
- 용도에 따라 분리해 운영체제가 더 효율적으로 관리할 수 있음
- 메모리 활용 효율
- 스택과 힙은 서로 증가하는 방향이 반대이기 때문에, 충돌 전까지 유연하게 확장 가능
- 디버깅 최적화
- 코드 부분은 공용 메모리에 올리고, 데이터, 스택만 따로 쓰게 된다면 메모리 절약이 가능함
스레드의 주소공간은 어떻게 구성되어 있을까요?
- 스레드는 프로세스의 스택영역에 상주하고있다. 각 스레드별로 독립적인 스택을 가지고있고, 각 스택은 분리되어져 있기에, 스레드 A의 지역 변수는 스레드 B가 접근할 수 없다 그렇기에, 함수 호출에 따른 지역 변수가 들어간다.
- 내가 헷갈린건데, 프레임 : 물리메모리 페이지: 가상 메모리, 프로세스는 프레임 단위로 짤려서 올라간다.
- 내가 헷갈린건 프레임도 커널, 유저 나뉘고 ㄴ하는줄 알았네
스택"영역과 "힙"영역은 정말 자료구조의 스택/힙과 연관이 있는 걸까요? 만약 그렇다면, 각 주소공간의 동작과정과 연계해서 설명해 주세요.
- 스택의 개념과 거의 동일하게 동작한다. 함수 호출을 하면 스택 프레임이 push 되고 함수 종료시 Pope되는것은 맞다.
- 위에서 아래로 증가하고, 지역변수, 함수 매개변수, 리턴 주소등이 저장된다. (규칙도 있음)
- CPU는 스택의 최상단 포인터(SP)를 추적한다.
- 하드웨어가 stack의 명령어를 수행할 수 있고, 컴파일 타임에 스택의 크기가 정해지기에 따로 시스템콜을 호출하지 않는다. 단순히 stack pointer(sp)만을 기준으로 판단한다.
- 힙영역은 전혀 연관이 없다.
- 아래에서 위의 주소로 증가하고, malloc, free, new, delete등의 래퍼 함수를 호출하면 공간을 할당한다.
- 내부적으로 free list(가용 리스트) + 메타 데이터(에필로그, 프롤르그, 푸터, 헤더)등의 정보로 관리하고, brk()와 mmap()등으로 힙 메모리를 확장시킬 수 있다.
IPC의 Shared Memory 기법은 프로세스 주소공간의 어디에 들어가나요? 그런 이유가 있을까요?
추가합시다 나중에
High Address
+-----------------------+ 0xFFFFFFFFFFFF
| Kernel space |
+-----------------------+
| Stack | ↓ grows down
| ... |
+-----------------------+
| Memory-mapped I/O | ← mmap(), shared memory
| Shared Libraries |
| Shared Memory IPC |
+-----------------------+
| Heap | ↑ grows up
| (malloc, brk, sbrk) |
+-----------------------+
| Data (BSS / Data) |
| Code (Text) segment |
+-----------------------+ 0x000000000000
Low Address
Heap과 Stack 사이에 있는 메모리 매핑 영역에 위치합니다. 결국 IPC는 생각해보면 스택에 들어갈 순 없고, 데이터 영역과 코드 영역은 정적 변수 / 텍스트 / 초기화되지 않은 데이터를 저장하는 공간이고 Heap은 Heap은 동적할당을 위한 공간이기에 이 데이터가 저장하는 곳은 Heap과 Stack 사이에 존재한다.
더 세부적인 이유로는 운영체제가 가상 주소공간의 빈 부분에 매핑해줘야 한다.
스택과 힙영역의 크기는 언제 결정되나요? 프로그램 개발자가 아닌, 사용자가 이 공간의 크기를 수정할 수 있나요?
기본적으로 Linux에선 스택의 크기를 8MB로 잡는다. O마다 다를 순 있지만, 일반적으로는 커널이 스택의 최대 크기를 설정한다.
사용자가 ulimit -s 명령으로 변경 가능하다. 하지만, 기본적으로 8MB이상의 영역을 할당할 수 없다.
힙은 고정적인 크기는 따로 없고, 가상메모리로 관리돌 수 있다. 왜냐하면, 연속된 메모리가 아니고, 푸터와 헤더에 의해 다음 주소를 가리키는 방식을 사용하기 때문이다 + 가용리스트이기 떄문이다. 그러면, 만약, 공간의 크기가 너무 커서 하나의 프로세스의 할당된 힙 영역의 크기를 넘어버린다면, 가상메모리까지 도입해서 관리할 수 있게 되는 것이다. (내 생각이었는데 정답입니다~ )