프로세스와 컨텍스트 스위칭
- 스케쥴러가 Process A에서 B로 바꿔주는 매커니즘을 컨텍스트 스위칭이라고 한다.
프로세스 구조
1. text(CODE): 코드
2. data: 변수/초기화된 데이터
- 두 가지 있다(BSS, DATA)
- BSS: 초기값 되지 않는 전역변수
- DATA: 초기값이 있는 전역변수
3. stack: 임시 데이터(함수 호출, 로컬 변수 등)
- 함수를 실행할 수 있는 구조를 만들어줌 "스택 프레임"
- PC(Program Counter) + SP(Stack Pointer)
- PC: 코드를 한줄 한줄 가리키는 주소 레지스터
- SP: 함수가 실행될 때 최상단 주소를 가리키는 레지스터
- EBP 레지스터: 프로세스 중 함수가 문제 생기면 어느 부분에서 문제가 발생했는지 빠르게 트래킹하기 위해 있다. 즉, 뻑 날 때 어느 부분에서 뻑 났는지 알려준다.
4. heap: 코드에서 동적으로 만들어지는 데이터
- malloc: 동적으로 메모리를 생성하는 함수
가볍게 쉬어가기
- 스택 오버플로우: 주로 해커들의 공격에 활용되었디.
PCB (Process Control Block)
스케쥴러가 프로세스 A가 실행 중 프로세스 B로 CPU실행을 바꾸면서 컨텍스트 스위칭 할 때 컨텍스트(PC+SP)가 각각의 프로세스마다 관리하고 있는 PCB 저장공간에 넣어둔다.
다시 B에서 A로 넘어갈 때 B의 컨텍스트를 PCB에 저장하고 PCB에 있는 컨텍스트를 진행중인 컨텍스트에 덮어 씌운다.
- Procress마다 ID가 있다.
- Register 값 (PC, SP 등이) 저장되어 있다.
- Scheduling Info (Process State) 프로세스 상태를 관리. 프로세스가 'ready,block,running' 상태를 알 수 있다.
- Memory Info(메모리 사이즈 limit)알 수 있다.
PCB: 리눅스 예
중간 정리
- 프로세스 구조
- Stack, HEAP, DATa(BSS, DATA), TEXT(CODE)
- PCB
- 프로세스 상태 정보 - PC, SP, 메모리, 스케쥴링 정보 등
Context Switching (문맥 교환)
- 실행 중지할 프로세스 정보를 해당 프로세스의 PCB에 업데이트해서, 메인 메모리에 저장
- 다음 실행할 프로세스 정보를 메인 메모리에 있는 해당 PCB 정보(PC, SP)를 CPU의 레지스터에 넣고, 실행
디스패치 (dispach): ready상태의 프로세스를 running상태로 바꾸는 것.
컨텍스트 스위칭 시간은 실제로 굉장히 짧은 시간(ms) 단위로, 프로세스 스위칭이 일어남.
쉬어가기
- 초기 컴퓨터 프로그램들은 어셈블리어로 작성
- 서로 다른 CPU 아키텍처가 등장할 떄마다 매번 똑같은 프로그램 작성
- 어셈블리어로는 프로그램 작성 속도가 매우 떨어짐
- 속도는 높으나 이식성이 떨어짐
- 컴파일러 등장
- CPU 아키텍처에 따라서는 컴파일러 프로그램만 만들면 됨, 기존 코드는 재작성할 필요 없음
- 그러나, 어셈플리어로 작성한 코드보다는 속도가 덜어질 수 있음
프로세스간 커뮤니케이션
프로세스간 커뮤니케이션할 수 있는 직접적인 방법은 없으나 IPC (Inter Process Communication)을 사용한다.
프로세스는 다른 프로세스의 공간에 접근할 수 없다. (다른 프로세스 데이터/코드를 바꿀 수 없다)
하지만 프로세스간 통신은 필요하다
성능을 높이기 위해 여러 프로세스를 만들어서 동시 실행할 때, 프로세스간 상태 확인 및 데이터 송수신이 필요하다.
fork() 시스템콜 (프로세스 안에 있는 C언어)
- fork()함수로 프로세스 자신을 복사해서 새로운 프로세스로 만들 수 있음. (이 함수를 호출하면 할 수록 많은 프로세스를 실행 할 수 있다)
- 여러 프로세스를 동시에 실행시킬 수 있음
- fork()를 호출한 프로세스를 부모 프로세스, fork()로 생성된 프로세스는 자식 프로세스
CPU가 한 개일 떄만 생각하지만, 최근에는 CPU안에 코어가 8개 되는 경우도 많고 각 프로세스를 각 코어에 동시 실행 가능(병렬 처리)
여러 프로세스 동시 실행하기 예
-
1~10000까지 더하기
- fork() 함수로 10개 프로세스 만들어서, 각각 1~1000, 1001~2000 더하기
- 각각 더한 값을 모두 합하면, 더 빠르게 동작 가능
- 단, 이 때 각 프로세스가 더한 값을 수집해야 하므로, 프로세스간 통신이 필요하다
웹서버 예
- 요청이 오면, HTML파일을 플라이언트에 제공하는 프로그램
- 새로운 사용자 요청이 올 떄마다, fork() 함수로 새로운 프로세스 만들고, 각 사용자 요청에 즉시 대응
- 각각의 프로세스가 각각의 CPU 코어를 사용하면서 각각의 요청을 빠르게 응답 가능
정리
- 여러 프로세스 동시 실행을 통한 성능 개선, 복잡한 프로그램을 위해 프로세스간 통실 필요
- 프로세스간 공간이 완전 분리
- 프로세스간 통신을 위한 특별한 기법 필요
- IPC (Inter Process Communication)
- 대부분의 IPC기법은 결국 커널 공간을 활용하는 것임
다양한 IPC 기법
- file 사용
- Message Queue
- Shared Memory
- Pipe
- Signal
- Semaphore
- Socket
2번부터는 모두 커널 공간을 사용한다.
프로세스간에 커널 공간을 공유한다.
파일을 사용한 커뮤니케이션
- IPC의 한가지 방법 중 하나로 file 사용
- shared.txt(file)
- 어떤 프로세스던 저장매체 공유 가능하고 사용도 가능하다.
하지만 한계가 있다.
- file을 사용하면, 실시간으로 직접 원하는 프로세스에 데이터 전달이 어렵다. 마냥 실시간으로 파일속 데이터를 일곡만 있을 수도 없다.
파이프 (pipe)
- 기본 파이프는 단방향 통신
- 부모 프로세스 ID값과 자식 프로세스 ID값이 다름
- fork() 로 자식 프로세스 만들었을 때만, 부무와 자식간의 통신이 가능
- if else문으로 자식 프로세스를 실행하게 할 수 있다
- 실제 데이터는 파일이 아니라 커널 공간 어딘가에 있다
메시지 큐 (message queue)
- queue니까, 기본으로 FIFO 정책으로 데이터 전송
- insert를 하면 넣은 순서데로 빼갈 수 있다
- message queue는 양방향, 어느 프로세스간에라도 데이터 송수신이 가능
- 먼저 넣은 데이터가, 먼저 읽혀진다
공유 메모리 (shared memory)
- 노골적으로 kernel space에 메모리 공간을 만들고, 해당 공간을 변수처럼 쓰는 방식
- message queue처럼 FIFO 방식이 아닌, 해당 메모리 주소를 마치 변수처럼 접근하는 방식
- 공유메모리 key를 가지고, 여러 프로세스가 접근 가능
pipe, message queue는 모두 kernel 공간의 메모리르 사용한다.
메모리 공간도 kernel/user로 구분된다.
시그널 (signal)
- 시그널 = 이벤트
- 커널 또는 프로세스에서 다른 프로세스에 어떤 이벤트가 발생되었는지를 알려주는 기법
- 프로세스 관련 코드에 관련 시그널 핸들러를 등록해서, 아래와 같은 것을 실행 할 수 있다.
i. 시그널 무시
ii. 시그널 블록(블록을 푸는 순간, 프로세스에 해당 시그널 전달)
iii. 등록된 시그널 핸들러로 특정 동작 수행
iv. 등록된 시그널 핸들러가 없다면, 커널에서 기본 동작 수행
주요 시그널
kill -l: 시그널 종류 리스트
- SIGKILL: 프로세스를 죽여라 (슈퍼관리자가 사용하는 시그널로, 프로세스는 어떤 경우든 죽도록 되어 있음)
- SIGALARM: 알람을 발생한다
- SIGSTP: 프로세스를 멈춰라 (Ctrl+z)
- SIGCONT: 멈춰진 프로세스를 실행하라
- SIGINT: 프로세스에 인터럽트를 보내서 프로세스를 죽여라 (Ctrl+c)
- SIGSEGV: 프로세스가 다른 메모리영역을 침범했다
이런 주요 시그널을 프로세스가 받게 되면 처리해야 되는 기본 동작을 운영체제 내에서 정의를 해놓았다.
시그널과 프로세스
PCB에 해당 프로세스가 블록 또는 처리해야하는 시그널 관련 정보 관리
시그널이 오면 Process PCB에서
- sigpending: 받은 시그널이 있다는 것을 간단하게 알려줌
- pending: 여러가지 시그널을 받으면 어떤 시그널이 대기 중인지 표현 해주는 자료구조 있고
- blocked: 프로그램 안에서 어떤 시그널이 blocked됐는지 알려주는 자료구조도 있고
- sig: 각각의 시그널에 대해 알려주는 자료구조 있다
실제로, 커널모드에서 사용자모드로 전환할 때 시그널을 처리한다.
자세히는, 커널 모드에서 사용자 모드로 다시 전환하는 마지막 시점에 해당 프로세스 PCB를 확인한다. PCB에 signal 자료구조를 보고, 만약 어떤 signal이 처리가 필요하다면 해당 처리를 담당하는 커널함수를 다시 호출해서 해당 함수를 실행한 다음에 사용자 모드로 전환된다. 만약, 해당 시그널을 처리해야 되는데 이 함수가 기본 동작이 아닌 특정 동작을 프로세스가 정의를 했다면 사용자 모드 안에 있는 프로세스에 있는 경우가 된다. 이 때는, 사용자 모드로 전환함과 동시에 특정 동작을 실행하도록 되어 있다.
소켓 (socket)
- 네트워크 통신을 위한 기술이다
- 클라이언트와 서버등 두 개의 다른 컴퓨터간의 네트워크 기반 통신을 위한 기술이다
- 하나의 컴퓨터 안에서, 두 개의 프로세슥나에 통신 기법으로 사용 가능하다
시그널과 소켓은 IPC를 위해서 나온 것은 아니지만 그 기능을 이용해서 IPC처럼 사용한다.
총정리
프로그램 실행 → 컴파일 → 실행파일 만들어지고 → 실행파일 실행하기 위해 쉘(CLI/GUI) 인터페이스를 사용해서 → 운영체제에 실행파일을 실행해달라고 요청한다 → 실행파일 프로세스는 STACK, HEAP, BSS, DATA, TEXT로 구성된다 → 실행 코드는 일단 TEXT(CODE)에 들어가게 된다 → 바로 실행하진 않고 스케쥴러 정책에 따라 다르게 진행된다 → (예시로, 스케쥴러 방식이 0.05초마 A와 B 컨텍스트 스위칭이고, 인터럽트 중에 하드웨어 타이머가 0.01초 마다 운영체제한테 인터럽트를 준다고 가정하자) → 인터럽트를 처리하기 위해 CPU는 사용자 모드를 커널 모드로 바꿔줌 → IDT라는 테이블에 들어가서 타이머 인터럽트에 해당하는 번호에 매칭된 커널 함수주소를 확인하고 실행한다 → 그러면 0.05초가 지나면 ready에 있던 프로세스가 running으로 컨텍스트 스위칭한다 → running 상태에 있는 프로세스 PCB 정보를 메이메모리에 저장하고 ready 상태로 변경 → 새로 들어오는 프로세스 정보가 new/ready일 경우 PCB(PC,SP)정보를 CPU에 넣어주고 이 프로세스의 코드를 CPU에서 실행하게 된다
메인 함수가 STACK에 들어가고 fd(지역변수)로 놓임 → 다음 줄 open()이라는 함수는 라이브러리에서 까봄 → open()이라는 함수는 시스템콜이라서 시스템콜을 처리하기 위한 mov eax, 1 // mov ebx, 0 // int 0x80 라는 특별한 함수가 들어가 있다 → eax는 시스템콜 번호가 들어고, ebx는 시스템콜 인자 (주소 정보 등)이 들어가고 int (CPU가 제공하는 opcode - CPU 명령어)에 인터럽트 번호 (0x80)을 주고 → int라는 명령은 사용자 모드에서 커널 모드로 바꿔주고 → IDT (OS가 부팅할 때 만들어지고)에서 각 번호바다 실행해야할 커널함수 주소가 적혀있고 → 시스템콜 같은 경우 0x80이고 system_call()함수를 호출한다 → system_call() 함수에서는 eax에 있는 번호를 확인해서 해당 번호에 매칭된 시스템콜 처리 커널함수로 이동하게 된다 → CPU레벨에서 DMA에 해당 저장매체에 있는 파일을 처리 부탁, DMA는 시스템 버스를 통해서 해당 저장매체에 접근해서 파일을 오픈한다 → 그 때, sys_open()함수가 실행되면 waiting으로 blocking되면서 더이상 프로세스가 실행되지 않게 된다 → DMA가 데이터를 다 받아 와서 CPU에 알려주면 인터럽트가 발생 → 인터럽트는 IDT에 가서 해당 인터럽트 번호에 맞는 커널함수를 실행 (wait에 있는 프로세스를 ready로 바꾸는 것) → 다시 정책에 따라서 ready에서 running으로 바뀐다 → 그런 후에야 코드의 다음 줄을 사용자 모두에서 실행할 수 있게 된다 → 그리고 프로그램이 끝나게 되면 running 상태의 프로세스가 exit하게 된다.