CPU는 특정 프로세스를 처리할 때 두 가지의 모드로 처리를 할 수 있다. 이는 Mode bit(1비트 or 2비트로 모드를 나타냄, 컨트롤 레지스터에 존재)를 통해 두 가지의 모드를 나타낸다.
두 가지 mode는 아래와 같다
- Kernal mode(Supervisor mode): 모든 권한을 가지고 프로세스를 처리한다.
- User mode: 제한된 권한을 가지고 프로세스를 처리한다.
- Ring 0: OS kernel
- Ring 1, 2: device drivers
- Ring 3: user
프로세스를 처리할 때 CPU는 번갈아가며 유저모드와 커널모드를 스위치한다. 그러므로 이 두 개만 확인할 것이다.
대개 OS들은 위와 같은 두 개의 ring만 사용한다.
이 두 가지 모드를 사용하는 주 목적은 시스템보호이다. 개별 사용자가 시스템을 수정을 할 수 있는 권한이 있으면 안 되기 때문이다. 게다가 커널모드에서만 사용할 수 있는 instruction이 존재한다.
CLI, STI instruction이라 불리운다.
제한되는 기능들을 알아보자. 대개 커널모드에서만 되는 기능이다.
제한된 instructuons이 존재한다. 커널모드로만 접근이 가능하다. 아래와 같은 수행을 한다.
- 하나의 자원을 접근제한하거나 가능하게 한다.
- CPU를 정지한다.
- mode bit을 변경한다.
- I/0 연산을 시작한다.
메모리 접근
- 유저 코드가 커널을 수정하는 것을 방지한다.
- 메모리의 어느 위치에 read/write 하는 것을 방지한다.
하드웨어 접근이 커널모드에서만 가능하다.
하드웨어 접근
- 커널모드로만 직접적으로 하드웨어에 접근할 수 있다.
프로그램 가능한 타이머 인터럽트- 커널모드로만 설정할 수 있다.
- 이는 context switches를 위해 사용되며 아래에서 알아볼 것이다.
Processors는 항상 한 가지 일만 한다.
CPU는 시작부터 끝까지 그냥 하나의 일인 instruction만 읽고 실행한다.
위와 같이 순차적으로 instrution을 수행한다.
일반적인 user모드에서 일어났던 것들을 확인해왔다. instruction이나 syscall 등
이는 근데, program에서 일어나는 것(program state)들을 처리하는 것이다.
지금 보는 부분은 system state에서 일어나는 변화들에 어떻게 대처하는지를 확인해 볼 것이다.
두 가지 모드 중 기존에 봤었던 부분이 유저모드 였다면 희미하게 보이는 부분이 커널모드이다.
커널모드로 들어가 특정 권한이 필요한 instruction을 처리할 수 있게 된다.
이러한 일이 언제 일어나는지 알아보자.
- 디스크나 network adapter에서 데이터가 전달될 때
- 시스템 타이머가 끝났을 때
- Page fault가 일어났을 때
Page fault는 디스트에 있는 데이터가 메모리에 로딩되지 않았을 때 이다.
다시 이미지를 보면 CPU가 예외적 이벤트(exception)가 발생했을 때, 커널모드에서 처리해야 할 예외적인 것들을 exceptional handler라고 한다. 이 핸들러의 주소로가서 예외적인 상황을 처리한다
이러한 커널모드로 들어가 함수나 코드가 실행되는데, 이 흐름을 exceptional control flow라고 부른다.
예외적 이벤트는 외부 또는 내부에서 모두 일어날 수 있다. 이를 exceptions와 interrupts로 나누어서 볼 것이다.
위의 사진을 보면 그냥 procedure call과 비슷하지만 다음 instruction이 실행되는게 보장되지 않는다.
segmentation fault도 커널모드에서 처리를 해주는데 이땐 프로그램이 중단되기 때문이다.
위의 이미지를 보면 user mode에서 커널모드로 변경되어 일이 수행되는데 리턴뿐만 아니라 Abort도 경우의 수중 하나이다. 이렇게 mode가 변경되는 것을 mode switch라고 한다.
여러가지의 Exception을 처리하기 위한 Table이 있다. 이들은 커널모드에서 사용되는 함수이며, 이들의 주소를 가지고 있는 array of function pointer이다. exception마다 인덱스가 부여된다.
Exception table을 interrupt vector라고도 부른다. 교재나 운영체제마다 이름이 다르다.
이제부터 exceptional control flow를 일으키는 exception을 interrupts와 exceptions로 나누어서 볼 것이다.
process의 외부에서 발생한 것에 의해 기인한다.
하드웨어에 외부 event를 받는 pin이 존재한다.
핸들러를 작동시킨다.
핸들러는 실행이 끝난다면 next intruction으로 돌아간다.
이미지를 보면 우측하단에 화살표 표시를 pin이라 해보자. interrupt signal이 여기에 발생한다면 handler가 interrupt vector 번호인 1번을 받으면 Timer interrupt를 이용하여 커널모드로 바꾸어 1번을 실행한다.
Timer interrupt
- 1ms마다 외부의 timer는 interrupt를 일으킨다.
- 유저모드에서 control을 가져오기 위해 커널이 사용한다.
I/O interrupts
- network에서 packet이 도작한다.
- disk로부터 데이터가 도착한다.
- Ctrl-c를 눌러 복사내용을 클립보드에 저장한다.
Interrupts와 다르게 내부적 요인으로 인해 발생하는 exception이다. 내부적 요인은 유저모드에서의 instruction이다.
Interntional exceptions: Trap
- 시스템에 의해 의도적으로 커널모드로 변경한다.
- 예시로 system call가 있다.(파일을 열기위한 함수들 open(), write(), read())
- control을 다음 instruction에 반환한다.
파일을 열었을 때 등이다.
Unintentional exceptions and recoverable: Faults
- 의도적이지 않지만 회복이 가능한 경우이다.
- 예시로 page faults가 있다.
- instruction이 다시 실행되거나, 종료한다.
Unintentional exceptions and unrecoverable: Aborts
- 의도적이지 않고 회복이 불가능하다.
- 메모리 접근을 잘못하거나 특권 명령어를 유저모드에서 사용하려할 때 이다.
ex) integer / 0- 프로그램을 중단한다.
Exception type마다 유일한 음이아닌 정수의 넘버가 부여된다.
컴퓨터가 부팅될 때, OS는 jump table을 초기화한다.
이 넘버는 테이블의 인덱스로 부여되며 이 테이블을 레지스터에 저장해놓는다. 이후 exception이 발생하면 이 인덱스를 통해 PC(다음에 실행시킬 명령어의 주소)를 exception handler 함수 코드 주소를 세팅하여 jump해 함수를 실행시킨다. 추가로 테이블의 첫 주소를 알면 인덱싱을 이용하여 함수를 실행시킬 수 있다.
virtual memory에서 더 다룰 것이다.
정상적으로 작동할 것 같다. 그런데, 메모리에 올라가지 않고 디스크에만 존재할 수 있다. 메모리에 없고,
그 때 메모리에 할당하고 재 실행을 한다.
충분히 복구 가능하기에 재실행된다.
메모리를 잘못 참조했을 때 발생하는 exception이다. 배열의 범위를 벗어난 곳에 접근하고 있기 때문에 segmentation fault가 발생하고 프로그램이 종료된다.
시스템 콜은 커널과의 interface이다.
system call은 process, fort, exac 등이 있다. 그리고 file I/O도 커널을 통해서만 받을 수 있다.
이 모두 시스템 자원을 보호하기 위함이다.
메모리에 접근하는 프로그램이나 버그가 있는 프로그램을 막기 위함이다.
reliability, security
위는 syscall table이다. 이것도 함수 포인터 배열이다. 다 커널 함수라고 볼 수 있다.
%eax 레지스터에 상수 2를 전달하는 것을 볼 수 있다 이는 2번 syscall을 수행해주세요라는 의미이다.
2번은 open()이다. 이 때 커널모드로 변경된다. 이는 Trap발생이다.
그리고 인자의 파일 이름이나 option(수정, 생성 등)은 %rdi, %rsi 레지스터에 저장이 된다.
리턴 값은 %rax에 저장된다. syscall은 보통 error가 많아 -1이나 0이면 에러를 의미한다.
위 이미지에서 int 0x80 instruction이 있다.. 이건 32bit에서만 존재한다. int는 interrupt라는 뜻이다. 0x80은 IDT에서 system call을 수행하기 위한 handling code가 존재한다.
write()를 호출한다면 라이브러리 코드에 write()는 1(console out)번이고 int 0x80인 syscall이 호출된다. trap이 발생하여 커널코드로 가게 된다. PC에 syscall을 공통적으로 수행하는 handler가 있다. 0x80이 exception handler code로 가게된다. 그럼 커널로 들어오게 된 것이다.
IDT는 exception table이다. systemp call이 호출된다면 128번에 있는 function table로 점프한다.
그 이후 syscall을 관리하기 위한 table이 존재한다. 그 syscall table의 인덱스를 가진 레지스터가 있고 그 레지스터의 번호를 통해 테이블을 참조하고 syscall을 호출한다.
참고로 모드가 바뀐다는 것은 프로세스가 바뀌는게 아니다. 그냥 function들의 집합으로 가는 것이다.
hello.c program의 write 라이브러리 사용예시를 보자. 1은 syscall handle 중 stdout이다. console로 출력한다는 뜻이다.
그리고 출력하고자 하는 문자열을 %rsi 레지스터에 저장한다. 두 번째 인자이다. 문자열의크기는 세 번째 인자인 %rdx 레지스터에 저장한다. syscall 부분은 OS가 syscall table까지 이동하여 sys_write를 호출하는 부분이다.
syscall이 정상적으로 작동되었기 때문에 %di에 있는 값 1을 xor 연산을 통해 0을 저장하는 모습이다. 0은 정상적으로 처리되었을 때의 bit이다.
지금 까지 Exceptuon handler는 외부에서 I/O device에서 interrupt가 발생했을 때,(그럼 PC는 interruept handler의 주소로 set이 된다.) instruction을 수행하다 비정상적인(exceptions) 작동이 되었을 경우, 의도적으로 syscall instruction을 이용해서 syscall을 호출해야할 때, syscall handler로 가는데 모두 커널 모드로 처리를 해야한다. 이를 Exception handler로 처리한 것이다.
Procedure call은 mode switch가 일어나지 않는다.
return address는 procedure call은 다음 isntruction을 수행할 수 있도록 next instruction의 주소를 가지고 있지만, Exception handler는 보장하지 않는다. error가 일어났을 경우 프로그램이 종료될 수 있고 mode switch을 야기한 current instruction를 재실행 시킬 수 있기 때문이다.
참고로 Kernal, User mode stack이 따로 있다.
- syscall은 다시 한 번 자원을 사용할 때, 시스템보호를 하기 위해서이다. 이를 운영체제가 커널모드로 대신 처리해주는 것이다. syscall의 범주는 다시 한 번 예시로, fork, exac, wait, exit 등 프로세스 관리를 위한 것이 있다. 모두 자원과 연관되어 있기에 자원이 다 없어져 에러가 발생할 수 있다.
- 사용자가 사용할 수 있게, 다양한 함수가 있는데, 이러한 함수들이 200~300개 밖에 없는 이유가 시스템을 보호하기 위함이다. 유지보수하기도 쉽고, 공격에 대응하기 수월해진다. 하지만 함수가 적기 때문에 매개변수 하나하나에 굉장히 많은 옵션이 있고 잘 다뤄야 한다. 이러한 인자들 때문에 오류가 종종 발생한다.
그렇기 때문에 이를 wrapping하여 좀 더 쉽게 사용할 수 있게 라이브러리로 syscall을 제공한다.- 권한 문제도 발생할 수 있다. 에러의 원인들이 굉장히 다양하기 때문에 제대로 사용한 것 같지만 내부적으로 권한 문제가 발생하여 에러가 발생할 수 있다.
- 동작 중에 에러가 발생할 수 있다. 외부의 요인으로 device와 호환이 되지 않아 발생한다.
이러한 error를 알기 위해서 전역변수로 error number인 errno을 정의해놓았다. error code 값이 이 변수에 저장이 된다. errno -l 옵션을 통해 에러 코드와 메세지를 통해 확인할 수 있다.
이는 error.h에 정의되어 있다. error code를 이 숫자를 직접적으로 코드상에서 사용하는게 아니라 이를 mapping한 매크로 상수를 사용하는게 권장된다. 자바는 try catch를 의무적으로 사용해야한다.
에러 처리의 예시이다. fork() 함수를 사용해 error 처리를 하고 있다. strerror는 error 넘버를 인자로 보내면 이에 상응하는 에러 메세지의 포인터 변수를 반환한다. fprintf는 파일에 출력하는 것이다.
리눅스 커널에 보면 2> 부분은 에러 메세지를 파일에 저장한다라는 뜻이다. &>는 출력메세지와 에러메세지 모두 저장한다는 뜻이다.
socket programming도 실제 코드량은 그렇게 많지 않다. 그런데 에러처리 코드가 실제 코드보다 더 길어지면서 가독성을 흐리게한다. 유닉스 스타일에선 에러 처리를 함수화 시켜서 코드의 길이를 줄인다.
그리고 이러한 에러처리를 함수화 시킨 함수가 존재한다. perror이다. stdio.h 라이브러리에 존재한다.
Stevens style이라 해서 fork 함수의 에러처리까지 한 번에 모아놓은 wrapper 함수 처리도 가능하다.