2023-04-18
운영체제 #2 — 운영체제 개요
앞 편이 기계 얘기였다면 이번엔 그 위에 올라앉는 소프트웨어 얘기다.
멀티프로그래밍과 멀티태스킹
CPU가 불필요하게 대기하지 않게 하려고 멀티프로그래밍을 한다. 어플리케이션 하나만으로는 CPU를 항상 바쁘게 만들 수 없으니, 대기 시간을 다른 어플리케이션을 실행하는 데 써서 자원 활용률을 높이자는 것이다.
이걸 하려면 여러 프로그램이 다 메모리에 올라와 있어야 하는데, 사실상 불가능하다. 그래서 virtual memory를 쓴다. 실행 중인 프로그램 전체가 메모리에 올라갈 필요 없이, 지금 당장 실행해야 하는 일부만 올린다. 프로그램 하나가 차지하는 공간이 줄어드니 더 많은 프로그램을 올릴 수 있다. 그래도 부족하면 메모리에 올라온 프로그램을 디스크로 내리고 새 프로그램을 올린다(swapping). 디스크에 써야 하니 오래 걸린다.
멀티프로그래밍이 되면 멀티태스킹까지 가능해진다. I/O가 없더라도 시간이 되면 실행 중인 프로그램에서 CPU를 강제로 빼앗아 다른 프로그램에 넘긴다. CPU 사용 시간을 여러 프로그램이 나눠 쓰는 것, 즉 time sharing이다. response time이 충분히 작아야 사용자 입장에서 동시에 돌아가는 것처럼 보인다.
정리하면 이렇다.
- 멀티프로그래밍 — CPU가 쉬지 않도록 여러 프로세스를 써서 이용률을 높이는 것.
- 멀티태스킹 — 여러 프로세스가 동시에 돌아가는 것처럼 보이게 하는 것.
OS operation
OS가 하는 일은 크게 세 갈래다.
하드웨어 인터럽트 핸들링. I/O 디바이스에서 오는 인터럽트를 처리한다. 나중에는 timer라는 디바이스도 I/O 없이 인터럽트를 보낸다. 키보드처럼 사용자가 언제 입력할지 모르는 장치를 위해 계속 무한 루프를 도는 건 비효율적이니, 입력이 들어왔을 때 인터럽트를 발생시켜 그때 처리한다.
소프트웨어 에러와 request. I/O와 무관하게 수행 중인 소프트웨어를 중단하고 긴급하게 처리해야 할 일이 생길 수 있다. division by zero 같은 것이다. CPU가 워낙 빠르기 때문에 에러의 파급 효과도 매우 빨리 퍼진다. 그래서 빨리 처리해야 한다. 이걸 exception 또는 trap이라 하고, 처리 방식은 interrupt handling과 비슷하다.
exception은 오류 상황, trap은 (핸들링을 위한) 요청에 의해 발생하는 것으로 구분한다. 넓은 의미의 interrupt가 hardware interrupt / exception / trap을 포괄하는 구조로 이해했다.
Dual mode. 어플리케이션은 사용자가 직접 짠 코드만 수행하지 않는다.
printf를 뜯어보면 마지막에 출력을 OS에 요청하는 명령이 있다. 화면에 출력하려면
디스플레이 장치에 접근해야 하는데, 하드웨어 접근은 보안을 위해 OS가 통제한다.
이때 OS에 요청하는 방법이 system call이다.
시스템 콜 코드는 OS 코드고, 사용자가 만든 어플리케이션 코드가 아니다. 권한이 다르다. OS 코드는 안전성이 보장되어 있으니 하드웨어에 직접 접근할 권한이 있고, 사용자 코드는 권한이 약하다. 그런데 CPU는 그냥 메모리에서 명령어를 가져와 실행할 뿐이라 그게 OS 코드인지 유저 코드인지 알 수 없다. 그래서 PSW의 mode bit를 쓴다. 유저 어플리케이션을 수행할 때는 user mode고, 시스템 콜을 호출하면 수행 전에 kernel mode로 바꾼다.
흐름은 이렇다.
user process 실행
→ system call 호출
→ trap 발생, mode bit = kernel mode
→ system call 실행
→ 끝나면 mode bit = user mode로 되돌리고 결과를 return
→ user process로 복귀
시스템 콜을 호출할 때마다 user → kernel 모드 전환이 일어난다.
OS의 구성 요소
프로세스 관리
프로세스란 실행 중인 프로그램이다. 정확히는 실행 중인 프로그램의 실체를 OS가 프로세스라는 이름으로 만든 것이다. 프로그램이 실행되려면 CPU, 메모리, I/O, 파일, 그리고 프로그램 안에서 정의된 데이터가 필요하다. 이것들을 하나로 묶어 관리하려고 OS가 자료구조를 제공하는데, 그게 프로세스다. 식판처럼 섹션이 나뉘어 있다고 생각하면 된다.
사용자가 더블클릭하면 OS는 프로세스 자료구조를 만들고 내용을 채워 메모리에 올린다. 그리고 CPU 스케줄링 대상에 포함시켜 달라고 등록한다. 스케줄링에 의해 CPU를 할당받으면 프로그램이 실행되고, 다 끝나면 프로세스가 terminate된다. 즉 자료구조를 삭제하는 것이다.
싱글 스레드 프로세스라면 PC 값을 하나만 가지고, 멀티 스레드라면 스레드당 하나씩 가진다.
프로세스 관리를 위해 OS가 하는 일은 이렇다. 프로세스를 만들고 삭제하는 일, 실행 중인 프로세스를 suspend하는 일, 프로세스 동기화, 프로세스 간 통신 제공, 그리고 deadlock handling이다.
메모리 관리
모든 데이터와 명령어는 메모리에 올라가야 한다. 여러 프로세스가 올라와 있으면
어떤 프로세스가 어디에 있는지, 어느 영역이 비어 있는지를 OS가 관리한다.
구체적으로는 메모리 layout 관리, swap 시 무엇을 올리고 내릴지 결정, 새 프로세스를
위한 메모리 할당, 실행 중 동적 할당(malloc 등)이다.
저장 장치와 파일 시스템
하드디스크의 공간은 너무 복잡해서 사용자가 직접 접근하기 어렵다. 그래서 OS가 간단한 방식으로 쓸 수 있게 해준다. "어플리케이션은 파일이라는 개념만 알아라, 하드디스크가 어떻게 생겼는지는 몰라도 된다"는 것이다. 파일에 쓰면 OS가 대신 저장한다. 우리가 파일에 데이터를 쓸 때 그게 디스크 어디에 저장되는지 모르는 이유다.
File-system management는 파일을 유지보수하는 방법, 저장하는 방법(그래서 directory 개념이 나온다), 그리고 사용자에게 제공할 operation(파일·디렉토리 생성/삭제, 데이터 변경, secondary storage에 저장, 백업)을 다룬다.
그 아래에는 mass storage management가 있다. 하드디스크 안의 빈 공간과 채워진 공간, 어떤 파일이 어디 있는지를 다 알아야 한다. disk scheduling도 여기 속한다. I/O 요청 순서를 조절해서 하드디스크 헤드의 움직임을 최소화하는 것이다.
캐싱과 일관성
하드디스크의 데이터가 CPU 레지스터까지 가는 경로는 하드디스크 → 메모리 → 캐시 → 레지스터다. 문제는 여러 프로그램이 동시에 돌고 있다는 것이다. 어떤 데이터가 캐시에 있을 수도 메모리에 있을 수도 있는데, 모든 프로그램은 그 데이터의 최신 값을 기반으로 동작해야 한다. OS는 이걸 보장해야 한다.
코어가 많아지면 더 어려워진다(cache coherency). 분산 시스템에서는 문제가 더 커진다. 같은 일을 여러 대가 나눠서 하니 같은 데이터를 쓰는데, 컴퓨터마다 값이 다를 수 있다. 이걸 고려해서 프로토콜을 짜야 한다.
I/O 관리
I/O 장치는 종류도 제조사도 다양하다. 그 특성들을 사용자가 일일이 구분할 필요가 없게 만드는 게 OS의 역할이다. 사용자는 아주 간단하게 block device인지 character device인지만 구분하면 된다. 하드디스크든 SSD든 USB든 CD-ROM이든, block device에 데이터를 쓰고 싶다는 요청만 하면 된다.
그리고 I/O 디바이스가 느린 걸 만회하는 소프트웨어적 기능을 제공한다. buffering, caching, 그리고 spooling이다. 프린터는 느리니까 hwp 파일을 출력할 때 spool 영역에 데이터를 써두면, hwp는 다른 일을 하고 OS가 프린터 속도에 맞춰 보낸다.
Protection과 Security
여기서 security는 공격에 대한 방어고, protection은 권한이 없는 자가 권한 밖의 자원에 접근하려 할 때 막는 메커니즘이다.
모든 사용자에게 userID를 주고 그 ID에 권한을 준다. 그 사람이 만든 파일은 기본적으로 그 사람에게 모든 권한이 있고, 다른 사람들에게 어떤 권한을 줄지 파일에 설정할 수 있다. 사용자가 로그인하면 그가 실행한 프로그램은 사용자의 권한을 그대로 승계한다. 같은 hwp라도 일반 사용자가 실행했느냐 root가 실행했느냐에 따라 권한이 다르다.
userID뿐 아니라 groupID도 있어서 사용자들을 묶어 그룹별로 권한을 줄 수 있다. 그리고 privilege escalation이 있다. 프로그램이 원래 없는 권한을 잠시 얻어야 일을 할 수 있는 경우다. print spooling이 그렇다. spool은 OS 영역이라 사용자가 함부로 쓸 수 없으니, print 작업을 할 때 프로그램에 권한을 잠시 부여한다.
Operating System Service와 시스템 콜
OS는 자기가 하는 일들을 사용자의 요청에 따라 해준다. 즉 사용자가 이런 일을
요청할 수 있도록 서비스를 제공한다. 프로세스를 만드는 일도 fork라는
시스템 콜 형태로 제공된다.
제공되는 서비스는 이런 것들이다. User interface, program execution, I/O operation, file-system 관련 서비스, communication(프로세스 간 데이터 교환, 네트워크를 통한 교환), 에러 detection과 처리, 자원 할당, accounting(어떤 사용자가 시스템 자원을 얼마나 쓰는지 추적), 그리고 protection과 security다.
시스템 콜은 primitive하다
시스템 콜은 매우 단순하다. 화면에 출력할 때 printf는 내부적으로 write()
시스템 콜을 부른다. 만약 write()를 직접 불러 출력하려면, write()가 워낙
primitive하기 때문에 여러 번 조합해서 호출해야 한다. 복잡하다. 그래서 printf
같은 high-level API를 통해 간단히 쓰게 만든다.
API를 쓰는 이유가 하나 더 있다. 시스템 콜은 OS가 제공하는 것이라, OS마다 하는 일은 같아도 이름과 사용법이 다르다. 시스템 콜을 직접 쓰면 OS마다 프로그램을 다시 짜야 하지만, API를 쓰면 소스코드는 하나로 만들 수 있다. (컴파일은 각 OS용으로 따로 해야 한다.) UNIX 계열은 POSIX API라는 동일한 API를 쓴다.
API는 두 종류다. C의 printf처럼 프로그래밍 언어가 제공하는 API는 이식성이
좋다. OS 계열에 따라 제공되는 표준 API는 이식성은 덜하지만 같은 계열에서는
공통으로 쓸 수 있다.
시스템 콜이 구현되는 방식
우리는 write()라고 부르지만, OS 내부에서는 각 시스템 콜에 번호가 붙어 있다.
그 번호에 해당하는 OS 코드의 위치를 알아야 점프할 수 있다. 그래서 index가 시스템
콜 번호인 테이블을 유지한다. 번호에 해당하는 index로 가면 시스템 콜이 위치한
메모리 주소가 적혀 있고, PC를 그 주소로 세팅해 점프한다.
printf를 호출하면 그 안의 write()가 호출되고, write()에 의해 커널 모드로
들어간다. write()가 return되면 그 값이 printf에 전달되고, 이를 바탕으로
printf의 return 값이 어플리케이션에 전달된다.
파일 복사 예제
cp 같은 명령을 생각해보자. 시스템 콜이 몇 번 불리는지 세어보면 감이 온다.
source file 이름을 키보드로 입력받고(I/O니까 시스템 콜), 사용자가 친 걸 화면에
출력하고(write), output file 이름도 같은 과정을 거치고, 입력받은 파일을
열고(open), output file을 만들고(create), 루프를 돌며 읽고 쓰고(read,
write 반복), 파일들을 닫고 완료 메시지를 출력하고(close, write), 마지막에
프로세스를 terminate한다(이것도 시스템 콜).
시스템 콜의 종류는 프로세스 관리, 파일 관리, 디바이스 관리, information maintenance(프로세스 id 같은 정보를 얻을 때), 통신, protection으로 나뉜다.
가상 머신
내가 쓰는 OS의 코드를 바꿔서 컴파일하고 실행했는데 오류가 났다고 해보자. 바꾸지 않은 OS를 다시 설치해야 한다. 오래 걸린다.
가상 머신은 컴퓨터 하드웨어와 그 위의 OS를 묶어서 하드웨어처럼 보이게 한다. 사실은 OS가 설치되어 있지만, OS가 깔려 있지 않은 bare hardware처럼 만드는 것이다. 그 위에 운영체제를 또 설치할 수 있다.
가상 머신이 없으면 hardware 위에 kernel이 있고 그 위에서 프로세스가 돈다. 프로세스는 system call interface를 통해 커널에 일을 시킨다. 가상 머신이 있으면 hardware 위의 kernel 위에 hypervisor가 있고, 그게 위쪽 프로세스들에게 가상 머신을 제공한다. VM은 하드웨어로 인식되므로 그 위에 또 커널을 설치할 수 있고, 그 OS의 system call interface를 쓰는 어플리케이션을 올릴 수 있다.
하나의 기계에 여러 운영체제를 설치할 수 있으니, 어플리케이션이 동작할 여러 환경을 한 컴퓨터에서 테스트할 수 있다. 클라우드 컴퓨팅은 이걸 이용해 여러 사용자가 한 컴퓨터를 공유하면서 각자 필요한 운영체제를 쓴다. 자원을 효율적으로 쓰는 방법이다.
JVM도 비슷한 발상이다. 컴파일은 기계어로 번역하는 것이라 한 번 컴파일하면 그 기계어를 읽을 수 있는 OS에서만 쓸 수 있다. 그래서 컴파일하지 않고 프로그램 자체를 보내서 사용자 PC에서 실시간으로 번역한다. 받은 class를 load하고 번역하는 게 JVM이고, JVM만 설치되어 있으면 기계가 무엇이든 실행할 수 있다.