System Call은 소프트웨어가 운영체제를 호출하는 것이다.
OS는 다양한 서비스들을 수행하기 위해 하드웨어를 직접적으로 관리하는 반면, 응용 프로그램들은 OS가 제공한 인터페이스를 통해서만 자원을 사용할 수 있다. 응용 프로그램들이 멋대로 하드웨어 자원을 사용하게 하기에는 꺼림칙하기 때문에 운영체제를 통해서만 하드웨어를 사용할 수 있도록 한다. 그때 사용하는 인터페이스를 시스템 호출이라고 한다.
fork()
유닉스, 리눅스 시스템 콜 중 하나이다. 새로운 프로세스 공간을 별도로 만들고, fork() 시스템 콜을 호출한 부모 프로세스 공간의 데이터을 모두 복사한다. 기존에 실행되고 있었던 부모 프로세스에 있는 모든 내용을 자식 프로세스에 copy한다.
프로세스 안에 있는 fork() 시스템 콜을 실행하면 새로운 프로세스 공간(동일한 코드, 데이터를 가짐)을 만든 다음에 fork() 다음 줄에 Program Counter가 놓여서 동일한 Code를 읽어나가게 된다.
#include <sys/types.h>#include <unistd.h>#include <stdio.h>int main()
{
pid_t pid;
printf("Before fork() call\n");
**pid = fork();**
if (pid == 0)
printf("This is Child process. PID is %d\n", pid);
else if (pid > 0)
printf("This is Parent process. PID is %d\n", pid);
else
printf("fork() is failed\n");
return 0;
}
자식 프로세스는 pid가 0으로 return 되고, 부모 프로세스는 pid가 실제 프로세스의 pid으로 return 되어 자식, 부모 프로세스를 pid를 이용해서 구분할 수 있다.
exec()
유닉스, 리눅스 시스템 콜 중 하나로 덮어씌운다. exec() 시스템 콜을 호출한 현재 프로세스 공간의 CODE, DATA, BSS 영역을 새로운 프로세스의 이미지로 덮어씌운다. 별도의 프로세스 공간을 만들지 않는다.
프로세스 안에 있는 exec() 시스템 콜 실행을 하면 새로운 프로세스를 만드는 것이 아니라 현재 만들어진 exec 인자에 들어가 있는 프로그램 실행 파일을 읽어서 현재 부모 프로세스 공간의 exec 인자에 있는 실행파일에 대한 CODE, DATA, BSS 영역을 덮어 씌운다. (HEAP, STACK 은 동적 영역이기 때문)
bind()
// 함수 원형
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
// 실제 사용
if(bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr))==-1){,,,}
bind() 함수에 3가지 인자를 전달함으로써 비로소 소켓에 주소를 할당할 수 있다.
즉, 우리가 앞서 socket() 함수로 받아온 디스크립터 sockfd가 존재하는데, 이 디스크립터 파일에 해당하는 소켓에 serv_addr 주소를 할당하겠다는 의미이다.
sockfd
: socket() 함수를 통해 배정받은 디스크립터 번호 serv_sockaddr
: IP주소와 PORT번호를 지정한 serv_addr 구조체addrlen
: 주소 정보를 담은 변수의 길이bind() 함수는 성공시 0, 실패시 -1을 반환한다.
listen()위의 예시에서는 큐의 크기를 5로 설정했으며, 이는 5개까지의 클라이언트 연결 요청을 대기시킬 수 있음을 의미한다.
bind를 통해 하나의 소켓에 ip주소와 port번호까지 할당했으니, 이제 클라이언트가 해당 소켓에 연결할 수 있도록 그 요청을 대기하는 상태로 만들어주어야 한다.
이 과정을 담당하는 함수가 listen함수이다.
즉, listen함수가 호출된 후 부터 클라이언트에서 connect(연결을 요청하는 함수)를 호출할 수 있게된다.
// 함수 원형
int listen(int sock, int backlog);
// 실제 사용
if(listen(serv_sock,5)==-1){,,,}
sock
: 소켓 디스크립터 번호backlog
: 연결요청을 대기하는 큐의 크기 즉, 지정한 디스크립터의 소켓이 리스닝소켓이 되며, backlog 만큼의 큐 공간을 갖는다.연결 요청을 대기하는 큐(Queue)
우선 '연결요청을 대기'한다는 것은 클라이언트가 연결을 요청했을 때, 그 요청을 대기시킬 수 있음을 의미한다. 그리고 큐는 쉽게 말해 대기실이라고 이해할 수 있는데, 시스템에서 순서대로(tcp인경우) 클라이언트를 연결시킬 수 있도록 큐에 모아 놓은 것이다.
accept()
이제 마지막으로, 대기중인 클라이언트의 요청을 차례로 수락함으로써 데이터를 주고받을 수 있게 된다.
accpet 함수가 바로 연결 요청을 수락하는 함수이다.
// 함수 원형
int accept(int sock, struct sockaddr*addr, socklen_t *addrlen);
// 실제 사용
clnt_addr_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size);
여기서 주의해야 할 점은, accept의 반환값은 성공/실패에 대한 정수값이 아닌, 새로운 디스크립터 번호라는 점이다.
즉, 우리가 앞서 이용하던 서버 소켓(리스닝 소켓)은 연결 요청을 대기시키는 과정까지를 담당하며, accept() 함수를 통해 새로 할당받은 소켓을 이용해 데이터 송수신을 할 수 있는 것이다.
sock
: 서버 소켓(리스닝 소켓)의 디스크립터 번호addr
: 대기 큐를 참조해 얻은 클라이언트의 주소 정보addrlen
: addr 변수의 크기응용 프로그램
응용 프로그램 실행 도중 하드디스크의 데이터를 읽어야 하는 상황이 발생한다.
시스템 콜 호출 요청
응용 프로그램이 커널의 서비스를 사용하기 위해 해당 기능을 나타내는 시스템 콜을 호출한다.
이를 위해 레지스터에 시스템 콜의 고유 번호를 저장하고 필요한 인자값을 설정한다.
인터럽트 발생
응용 프로그램이 시스템 콜을 호출하면, 해당 시스템 콜의 고유 번호를 포함한 정보를 가지고 인터럽트를 발생시킨다.
CPU는 유저 모드에서 커널 모드로 전환된다.
기존 프로세스 컨텍스트 저장
현재 실행 중인 프로세스 상태, 레지스터 값, 메모리 주소 등을 해당 프로세스의 PCB에 저장한다.
시스템 콜 처리 이후 복구하기 위한 단계이다.
인터럽스 서비스 루틴 실행
사용자 모드 복귀
시스템 콜이 성공적으로 처리되면 커널은 Interrupt Return 명령어를 사용하여 커널 모드에서 사용자 모드로 전환하고 응용 프로그램이 시스템 콜을 호출한 위치로 돌아간다.
!https://velog.velcdn.com/images/nnnyeong/post/5ffac049-22e2-486e-8d4d-11f7f0d31778/image.png
우리가 일반적으로 사용하는 프로그램은 응용 프로그램이다.
유저 레벨의 프로그램은 유저 레벨의 함수들만으로는 많은 기능을 구현하기 힘들기 때문에, Kernel의 도움을 반드시 받아야 한다. 이러한 작업은 응용 프로그램으로 대표되는 유저 프로세스에서 유저 모드로는 수행할 수 없다. 반드시 Kernel에 관련된 것은 커널 모드로 전환한 후에야 해당 작업을 수행할 권한이 생긴다.
유저 모드 | PC Register가 사용자 프로그램이 올라가 있는 메모리 위치를 가리키고 있을 때 |
---|---|
커널 모드 | PC Register가 운영체제가 존재하는 부분을 가리티고 있을 때 |
만약 권한이 없는데도 해커가 피해를 입히기 위해 악의적으로 시스템 콜을 남용하거나 초보 사용자가 하드웨어 명령어를 잘 몰라서 아무렇게 함수를 호출했을 경우, 시스템 전체를 망가뜨릴 수도 있기 때문이다.
따라서 이러한 명령어들은 특별하게 Kernel Mode에서만 실행할 수 있도록 설계되었고, 만약 유저 모드에서 시스템 콜을 호출하는 경우에는 운영체제에서 불법적인 접근이라고 여기고 트랩을 발생시킨다.
각각의 시스템 콜은 고유한 번호를 가지고 있는데, syscall에 이 시스템 콜의 번호를 입력하는 방식으로 호출한다. 내부적으로 이 syscall은 0x80 인터럽트를 이용해서 커널에 명령을 전달한다. 따라서 시스템 콜의 고유 식별 번호를 통해 구분할 수 있다.