📣
해당 포스트는 운영체제 공룡책과 고건 교수님의 OLC 강의 등을 참고하여
작성되었습니다.
시스템콜을 한마디로 정리하면 커널에 접근하기 위한 인터페이스입니다.
하지만 시스템콜이 이 한마디로 이해되셨다면 시스템콜을 검색하지 않으셨겠죠?
한단계씩 자세히 풀어나가보겠습니다!
1. 시스템콜이 생긴 이유
먼저 시스템콜이 무엇인지 알아보기 전에 시스템콜은 왜 생기게 되었는지 한번 알아볼게요!
개인용 PC로 시작된 Window와는 다르게 Linux는 하나의 컴퓨터를 여러명의 사용자들이 사용하기 위한 OS로 만들어졌습니다.
여러명의 사용자이면서 여러개의 프로그램이라고도 할 수 있겠죠? 사용자들은 결국 프로그램을 실행하는 사람이니깐요ㅎㅎ
이를 Multi User System 이라고 합니다!!
반면에 window는 Single User System이라고 하죠ㅎㅎ
근데 그거 아시나요?
원래 Linux도 개인용 컴퓨터를 위한 OS로 시작되었다는거?? (GUI로 시작했다는 것만 봐도...그럴만 하죠?)
근데 Linux가 오픈소스잖아요?
사용하는 측에서 이것도 붙여보고 저것도 붙여보고 필요에 맞춰서 개선할 수 있는거죠!
그래서 서버 컴퓨터를 위한 OS로 많이 쓰이게 되었고, 점차 멀티 유저 사용환경으로 발전했다고 합니다!
다시 돌아가서 이야기하자면,
하나의 컴퓨터를 여러 명의 사람이 사용,
즉 멀티 유저 시스템을 사용하면 뭐가 문제일까요??
상상을 해봅시다.
내가 좋아하는 사람이 생겨서 컴퓨터로 막 열심히 편지를 작성해놨습니다.
근데 갑자기 친구한테 카톡이 옵니다.
" 너 팜하니 좋아해? "
와 어떻게 알았지??
그 친구가 컴퓨터에 접속해서 메일 보내려다 첨부파일 삽입을 눌렀는데 리스트에 내가 쓴 편지가 있었다는거죠.
이처럼 멀티 유저 시스템에서는 다른 사람, 혹은 다른 프로그램이 내 메모리와 디스크에 있는 정보를 조회하거나 수정하는 끔찍한 일이 일어날 수도 있는거죠!
컴퓨터의 물리적인 메모리와 디스크는 하나니깐요!!
그래서 먼 이전에 우리 선배님들은 이렇게 생각한거죠
"파일 I/O 작업을 할 때는 무조건 커널의 허락을 받게 하자"
리눅스에서의 파일 I/O 작업이란 키보드를 치거나, 디스플레이에 띄우거나, 디스크를 읽거나, 네트워크 통신을 하거나 등등을 의미합니다.
구체적인건 조금 이따가 설명해드릴게요!
또 커널이란 운영체제의 구성요소인데, 항상 메모리에 상주해 있는 프로그램입니다. 운영체제의 핵심이라고 보면 되요.
만약에 친구가 메일을 보내려고 첨부파일 삽입을 클릭해서 리스트가 뜨기 전에 커널의 허락을 받아야 했다면 어땠을까요?
아마 커널을 이렇게 대답했을 겁니다.
"너 메모리 영역 아니니깐 안돼! 돌아가!"
그럼 편지는 안전했겠죠??
이렇게 커널에게 I/O 작업을 의뢰하는 이 행위를 시스템콜이라고 합니다.
배경이 이해가 되셨나요?
그럼 이제 이런 의문이 드실거에요!
그래서 시스템콜은 누가 날리는거고 어떻게 날리는거지? 날리는 상황은 어떻게 판단하는거지?
이제 차근 차근 알아봅시다!
아! 컴퓨터 구조는 알고 계신다는 가정 하에 설명할게요!
레지스터가 뭐고 메모리가 뭐고 디스크가 뭐고 이런건 알고 계셔야 합니다!
2. CPU가 하는 일의 시퀀스와 Mode Bit (모드 비트)
CPU는 사용자모드와 커널모드를 왔다갔다 하면서 일합니다.
무슨 말이냐고요?
일단 그전에 CPU가 일을 어떻게 하는지부터 볼까요?
사실 프로그램이라는 것은 정말 간단한 명령들의 집합입니다.
우리가 C나 Java Swift와 같은 고차언어로 어려운 프로그래밍을 막 해놓더라도
결국에 컴파일러가 바꿔놓은 명령어는 '이 레지스터에서 저 레지스터로 옮겨라', '더해라', '빼라', '인터럽트를 발생시켜라', '메모리에서 가져와라' 등등 밖에 없거든요!
CPU들이 저 명령어들을 엄청 빠르게 수행하면서 우리가 보는 프로그램의 동작들이 되는거죠!
CPU가 명령어들을 빠르게 수행한다고 했었는데,
명령어들은 어디에 있을까요??
바로 메모리입니다.
왜 메모리에 있을까요?
우리가 프로그램을 실행하기 위해서는 프로그램이 Loading되어야 합니다.
Loading... 많이 들어보셨죠?
바로 디스크에 저장되어 있는 프로그램을 (일단 필요한 부분만) 메모리에 올리는 작업을 Load(적재)라고 합니다.
그래서 메모리에 있는거에요!
아.. 다 아시는거라고요? 그럼 넘어갈게요!
그래서 CPU가 하는 일을 다시 정리해보면...!
1. CPU는 끊임없이 주소값으로 메모리에 접근해 명령어를 Fetch하고,
2. Fetch된 명령어를 Decode해서 어떤 명령어인지, 뭘 해야하는지 결정하고
3. Decode한 대로 Execute 해서 무언가를 해내고!
4. 그 해낸 결과를 Writeback 해서 레지스터나 메모리에 기록합니다.
5. 그리고 이 과정을 계속 Repeat하는 것이죠! 겁나 빠르게!
휴... 컴퓨터구조 이야기 안한다고 해놓고 다 했죠?
어쩔 수 없습니다. 제가 설명충이라
근데 CPU가 어떻게 일하는지를 최소한은 알아야! 이해할 수 있어서 이것도 어쩔 수 없었습니다.
혹시 알아요? 이 복습이 면접에서 CPU를 기억나서해서 합격시킬지?
아무튼! 다시 돌아와서!!
Fetch 단계에서는 명령어를 가져온다고 했죠?
프로세스는 자신이 할당 받은 메모리 영역 내의 명령어만 가져와야 합니다.
근데 우리 프로그램이 가끔씩 Disk write이나 Disk read 등의 시스템 함수를 실행해야 할수도 있죠?
그때는 커널로 CPU를 뺏기고 커널이 해당 작업을 해줍니다.
이렇게만 이야기하니까 잘 이해가 안되죠??
일단 차근차근 Mode Bit 부터 봅시다ㅎㅎ
먼저 사용자 모드와 커널 모드에 대해서 알아야 합니다.
먼저 우리의 프로세스는 항상 두개의 Stack 메모리 구조를 가지고 있습니다.
첫번째 stack은 우리가 잘 아는 프로세스의 메모리에 할당되어있는 유저 메모리 스택입니다.
두번째 stack은 커널에 있는 해당 프로세스 PCB에 있는 커널 메모리 스택입니다.
만약에 덧셈을 한다고 했을 땐, add() 함수가 유저 메모리 스택에 들어가서 pop되면서 작업을 수행하는 거죠.
그리고 그 결과를 디스크에 기록해야 할 때는 sys_disk_write() 함수가 커널 메모리 스택에 들어가서 pop 되면서 작업을 수행하는 것입니다.
이 친구는 file I/O 함수니까요!
즉, 사용자 모드와 커널 모드에서 일어나는 일 모두, 현재 프로세스의 스택을 사용하지만 현재 프로그램 영역 내에서 수행되느냐, 아니면 커널 영역 내에서 수행되느냐의 차이점이 있는것이죠!!
이해가 되셨나요??
방금 말씀드린 사용자 모드와 커널 모드간의 문맥 교환이 가능하도록 도와주는 모드 비트에 대해서 알아볼게요
드디어 등장... CPU에는 모드 비트라는 것이 있습니다.
모드 비트는 CPU안에 진짜 조그마한 바이너리 비트인데,
1이면 커널모드라서 모든 영역의 메모리에 접근할 수 있습니다.
근데, 0이면 사용자 모드라서 지금 현재 돌아가는 프로세스의 Local한 메모리, 그러니까 이 프로세스가 할당받은 메모리에만 접근할 수 있습니다.
할당 받은 메모리인지는 어떻게 아느냐? 바로 MMU가 검사를 합니다.
(MMU에 대해서는 조만간 메모리를 설명할 때 다루는걸로 하고 일단 넘어갈게요!)
만약에 할당 받은 영역 이외의 메모리에 접근하면 Hardware Exception을 날리고 프로그램 종료나 다른 어떤 적절한 처리를 해주는거죠!
이건 해킹 시도나 다름 없으니까요..!
일단 우리는 모드비트라는 게 있는 건 알았고!
사용자 모드일 때는 프로세스에게 할당된 메모리 밖의 명령어에 접근하는 것이 불가능하다는 것을 알았고!
커널 모드일 때는 전체 메모리 상의 어떤 명령어에도 접근할 수 있다는 것을 알았어요
그럼 커널 모드는 어떤 경우에 되는 것이고, 왜 메모리의 모든 영역의 명령어에 접근할 수 있어야 하는지 궁금하실거에요!
먼저 아까 말씀드렸듯이 이 커널모드로의 전환은 시스템콜에 의해 일어납니다.
그럼 커널 모드라는 게 왜 필요한지는 지금부터 설명 드릴게요!!
3. 시스템 콜은 왜? 그리고 어떤 과정으로 일어나는 것일까?
아까 편지 이야기를 하면서 우리의 선배님들이
"파일 I/O 작업을 할 때는 무조건 커널의 허락을 받게 하자"
라고 결정했다고 했죠?
사실 시스템 콜은 파일 I/O 외에도, 생성된 자식 프로세스 메모리를 할당하거나 프로세스가 block되어 대기하게 하는 역할 등등도 하지만
가장 중심적인 역할이 파일 I/O 요청이라서 이것에 집중해보려고 합니다!!
리눅스에서 파일은 단순히 텍스트, 이미지, 영상, 실행파일 이런 것 뿐만 아니라
sequence of bytes
즉, 바이트가 흘러가는 모든 것들을 뜻해요!!!
그 말인즉슨, 우리가 키보드를 누르면서 신호를 1byte씩 보내는 것 뿐만 아니라,
모니터에 표시되는 정보들도 모두 sequence of byte로서 파일입니다.
심지어 네트워크를 통해 들어오는 데이터도 sequence of byte, 즉 파일입니다!
그리고 리눅스는 이 모든 것들을 fd (file descriptor)로 관리를 하죠??
이것만 봐도 우리의 프로그램은 커널 모드에 들어가지 못한다면 할 수 있는게 얼마 없습니다.
갑자기 궁금해져서 계산을 해봤는데,
제가 집에서 쓰는 모니터가 4K 60Hz이거든요?
픽셀수를 계산해보니까 8,294,400개더라고요!
근데 RGB가 각각 R: `2^8`, G : `2^8`, B: `2^8` 라서 총 24비트이고 그럼 픽셀 하나당 3바이트라고 하고 계산해보면
총 24,883,200바이트더라고요
구글의 힘을 빌려서 계산 해보니까 24.8MB의 정보를 표시하는거고,
60Hz니까 곱하기 60해서
1초에 약 1493MB 만큼의 데이터를 쓰더라고요.
약 1초에 1.4GB ... 엄청난 데이터양인 것 같아요!
글이 너무 길어서 잠 깨시라고 다른 이야기를 해봤습니다!!ㅎㅎ
우리는 방금 파일 I/O 작업 때문에 커널의 도움이 꼭 필요하다는 것을 알았습니다.
이제부터 조금 어려운 이야기를 해볼게요!!
바로 시스템 콜이 일어나는 과정!! 입니다!
마지막에 한번 더 정리 해드릴테니 끄덕 끄덕 하시면서 쭉 따라오시면 됩니다!!
예시는 printf 함수를 통해 들어볼게요!!!
int main(void) {
...
printf("Hello, my name is Sdaq\n");
...
}
자 이런 코드가 있습니다.
여기서 printf는 라이브러리 함수입니다. standard inout 라이브러리 (stdio.h)에 있는 함수죠?
이 코드는 결국 다음과 같습니다.
int main(void) {
...
printf() {
...
write(...)
...
}
...
}
printf 함수 내부에 System call 함수인 write가 있죠!!
자 여기까지는 C언어를 하신 적이 있다면 쉽게 이해하실 수 있으실거에요!
저 코드를 컴파일하고 뜯어보면, 실제로는 wirte 하는 명령어는 없습니다!
왜냐면 write는 I/O 명령어이고 사용자 모드에서는 실행할 수 없기 때문이죠!
만약에 그런 명령어가 컴파일된 실행파일 속에 있다면,
아마 Hardware Exception이 발생하고 오류가 나게 될거에요!!
그럼 저 write함수 부분이 컴파일 되고나면 어떤 느낌인가를 봤을 때!
... <- (1)
movl 5, %eax <- (2)
int $0x80 <- (3)
대략 이런 명령어들로 이루어져 있습니다.
(1)은 매개변수와 관련된 명령들이고, 중요하게 봐야 할 부분은 (2), (3)이에요!!
먼저 (3)번 부터 설명을 드릴게요!!!
자 우리의 CPU가 명령어를 Fetch해서 Decode를 한 후, Execute를 하겠죠?
저때 int $0x80는 시스템 콜을 트리거하는 명령어입니다. 좀 더 엄밀히 말하면 인터럽트를 발생시키는 명령어죠!
int는 interrupt의 줄임말입니다!
무조건 interrupt를 발생시켜야해??
응 무조건!
자 인터럽트가 발생했으니, 일단 CPU는 자연스럽게 커널로 넘어가게 됩니다.
그리고 모드 비트도 1로 바뀌게 되죠!!!
그리고 인터럽트는 인터럽트 핸들러가 처리를 해야겠죠?
여기서 불린 인터럽트는 시스템 콜을 처리해달라고 부탁하는 인터럽트입니다.
단순히 시스템 콜을 처리해줘!! 라고 했기 때문에 어떤 시스템 콜을 처리해야 하는지는 알 수 없는 상황입니다.
그렇다면 어떤 시스템 콜인지는 도대체 어떻게 알 수 있을까요??
바로 (2)에서 관련된 정보를 약속된 레지스터에 저장해두었기 때문에 알 수 있습니다.
(2)는 EAX 레지스터에 값을 옮기는 작업입니다.
어떤 값을 왜 옮긴걸까요??
시스템 콜은 sys_ call_table을 참조하여 어떤 일을 할지를 결정합니다.
이 참조는 index를 통해 이루어지는데, EAX 레지스터를 참고하기로 약속했기 때문에
write 시스템콜에 해당하는 인텍스 값을 EAX 레지스터로 옮긴거죠!
시스템 콜 테이블은 인덱스마다 해당 함수의 첫번째 주소값을 가지고 있기 때문에, sys_write 함수의 첫번째 주솟값을 통해 실행할 수 있겠죠?
좀 복잡한가요? 정리하자면 이렇습니다ㅎㅎ
인터럽트는 인터럽트 핸들러가 처리하는데,
이 인터럽트는 시스템 콜을 처리해달라는 인터럽트이다.
핸들러는 어떤 시스템 콜인지를 약속된 레지스터에서 시스템콜 테이블에서 인덱스로 확인을 하고 처리한다.
시스템 테이블은 해당 함수의 시작 주소값을 가지고 있고, 그 주소값을 통해 시스템 콜 함수를 실행한다!
근데 이걸 실행할 수 있는건, 인터럽트로 CPU가 커널로 넘어가서, 모드 비트가 1이기 때문이다!
와 너무 깔끔했다...!
이제 진짜 마지막으로 시스템 콜이 일어나는 총 과정을 컴파일 단계와 런타임 단계로 나눠서 정리해드릴게요!
컴파일 단계
먼저 실행 전에, 컴파일 타임에 컴파일러는 라이브러리 함수인 printf를 찾습니다.
이 함수 내부에는 write 시스템 콜이 있는데,
이 wirte 시스템콜을 실행시키기 위해서 필요한 코드를 컴파일러가 대신 적어줍니다.
... <- (1)
movl 5, %eax <- (2)
int $0x80 <- (3)
(1) ... 매개변수들을 저장하는 작업을 합니다. 여기서는 write안에 들어가는 매개변수들이 레지스터에 저장이 되겠죠?
(2) movl 5, %eax 은 트랩이 발동되기 전에 어떤 시스템콜 때문에 트랩이 발동되었는지를 알려주는 정보를 기록하는 것입니다.
write 시스템콜 함수에 대응하는 sys_ call_table의 index인 5를 EAX레지스터에 저장한다는 뜻이죠. (EAX에 저장하는 이유는 그곳이 약속된 레지스터이기 때문에!)
이렇게 5라는 Table index로 변환시키는 작업은 컴파일 단계에서 이루어집니다.
(3) int $0x80 같이 시스템콜을 부르는 인터럽트 트랩을 발동하는 주소를 던지는 코드를 넣어둡니다.
런타임 단계
런타임 단계에서는 저 컴파일된 한 줄 한 줄이 실행되며 트랩이 걸려서 커널 안에 있는 트랩 핸들러가 작동합니다.
핸들러는 EAX 레지스터에서 인덱스를 찾아서 시스템콜 테이블에서 그 인덱스의 값(주소)이 가리키는 시스템 함수를 발동합니다.
그리고 작업이 다 끝나면 유저모드로 되돌아갑니다.
시스템콜 끝!!
되셨나요?? 긴 글 따라오느라 정말 수고 많으셨습니다.
물어보실게 있으시다거나 더 알려주거나 고쳐주고 싶은게 있으시다면 댓글 부탁드릴게요!
<참고 자료>
운영체제 10판
운영체제 - YES24
운영체제
www.yes24.com
고건 교수님 OLC
OLC CENTER
olc.kr
'운영체제' 카테고리의 다른 글
[운영체제] 인터럽트(Interrupt) 핸들링을 파보자! (0) | 2023.02.09 |
---|---|
[운영체제] Linux의 CPU 스케줄링 (CFS 알고리즘)을 파보자! (4) | 2023.02.08 |
[운영체제] 프로세스 생성(fork)을 파보자! (3) | 2023.02.06 |