본문 바로가기
컴퓨터 과학/운영체제

운영체제 인터페이스

by pagehit 2021. 7. 4.
반응형

이 글에서는 운영체제에 대해 간략히 알아보고, 운영체제 인터페이스(Operating Systems Interfaces)에 대해 알아본다. 책 xv6: a simple, Unix-like teaching operating system을 참고하여 정리한다.

운영체제는 다수의 프로그램이 컴퓨터라는 한정된 자원을 공유하도록 만든다. 또한, 저수준(low-level) 하드웨어를 추상화하고 관리한다. 따라서 인터넷 브라우저와 같은 프로그램은 구체적으로 어떤 하드웨어를 사용하고 있는지 신경쓸 필요가 없다. 운영체제가 알아서 어떤 하드웨어를 사용할지 관리해 준다.

운영체제는 또한 프로그램들이 서로 같이 일하거나 같은 데이터를 공유할 수 있도록 만든다.

운영체제는 인터페이스를 통해 사용자 프로그램(user program)에게 서비스(service)를 제공한다. 인터페이스는 구현하기 쉽게 간단해야 하지만, 응용을 위해 복잡한 기능들을 제공해야 하기도 하는 양면성을 가지고 있다. 이 둘을 충족시키기 위해 일반적인(general) 기능들을 제공할 수 있도록 인터페이스를 설계한다. 양면을 다 만족해야 하기에 인터페이스 설계는 쉬운 일이 아니다...


xv6는 유닉스 운영체제에 있는 기본적인 인터페이스를 제공한다. 유닉스 운영체제의 인터페이스는 BSD, Linux, Mac OS X와 같은 현대 운영체제의 인터페이스와 거의 같다.


커널(kernel)은 실행중인 프로그램에 서비스를 제공하는 프로그램이다. 각각 실행되는 프로그램을 프로세스(process)라 하며, 이 프로세스들은 명령어(instruction)와 데이터(data), 스택(stack)을 포함하는 메모리를 가진다. 명령어는 프로그램의 특정 연산을 구현하며, 데이터는 연산에 사용되는 변수이다. 스택은 프로그램의 함수 호출을 구성한다. 컴퓨터는 다수의 프로세스를 가지지만 하나의 커널을 가진다.

프로세스가 커널 서비스를 사용하고 싶으면, 시스템 콜(system call)을 호출한다. 시스템 콜은 운영체제 인터페이스 중 하나이다. 시스템 콜을 호출하면 커널로 들어가고, 커널에서 서비스를 수행한 뒤 반환한다. 즉 프로세스는 사용자 공간(user space)과 커널 공간(kernel space)을 왔다 갔다하며 실행된다.

커널은 CPU가 제공하는 하드웨어 보호 기능을 사용하여 사용자 공간에서 실행되는 프로세스가 자신의 메모리만 접근할 수 있도록 한다. 커널은 이러한 보호 기능을 구현하는데 필요한 권한(privilege)을 가지고 실행된다. 반면 사용자 프로그램은 이러한 권한이 없다. 사용자 프로그램이 시스템 콜을 호출하면, 하드웨어는 권한을 상승시키고 커널에 있는 함수를 실행한다.

커널이 제공하는 시스템 콜은 사용자 프로그램의 관점에서 바라 보면 인터페이스이다.

쉘(shell)은 사용자의 명령을 읽어 실행하는 평범한 프로그램이다. 쉘은 커널의 부분이 아닌 사용자 프로그램이다.


프로세스와 메모리(Processes and memory)

xv6의 프로세스는 사용자 공간 메모리(인스트럭션, 데이터, 스택)와 커널에 대한 각 프로세스 상태로 구성된다. xv6 프로세스를 시분할방식(time-sharing)으로 사용한다. 사용 가능한 CPU가 실행 대기중인 프로세스들 사이를 바꿔가며(switching) 사용한다. 프로세스가 실행되지 않을 때, xv6는 CPU 레지스터(register)를 저장하고, 해당 프로세스를 다음에 실행할 때 레지스터를 복원한다. 커널은 각 프로세스에 대해 PID(process identifier)를 매겨 구분한다.

프로세스는 fork 시스템 콜을 이용해 새로운 프로세스를 만든다. 새로 생성된 프로세스를 자식 프로세스(child process)라 하며, 프로세스를 호출한 부모 프로세스(parent process)와 정확히 같은 메모리 컨텐츠(contents)를 가진다. fork는 부모와 자식 프로세스 둘 모두에게 반환된다. 부모 프로세스에서는 자식의 PID가 반환되며, 자식 프로세스에서 fork의 반환값은 0이다. 

int pid = fork(); // 프로세스를 생성하고 PID를 반환한다
if (pid > 0) { // 부모 프로세스에서는 자식 프로세스의 PID가 반환된다
    printf("parent: child=%d\n", pid);
    // wait을 통해 자식 프로세스가 종료(exit)되길 기다린다
    // 종료 상태(exit status)는 *status에 저장되며, 자식의 PID를 반환한다
    pid = wait((int *) 0);
    printf("child %d is done\n", pid);
} else if (pid == 0) { // 자식 프로세스에서 fork의 반환값은 0 이다
    printf("child: exiting\n");
    exit(0);
} else {
    printf("fork error\n");
}

 

exit 시스템 콜은 시스템 콜을 호출한 프로세스의 실행을 멈추고 메모리와 열려있는 파일 등과 같은 자원을 회수한다. exit은 정수형 상태 인자를 받으며, 0은 성공, 1은 실패를 나타낸다.

wait 시스템 콜은 현재 프로세스의 종료(exited or killed)된 자식 프로세스의 PID를 반환하고, 자식의 종료 상태(exit status)를 함수에 넘겨진 주소에 복사한다. 호출자(caller)가 자식이 없으면 즉시 -1을 반환한다. 부모가 자식의 종료 상태를 신경쓰지 않을 경우, 함수에 주소 0을 전달한다.

처음에는 자식과 부모 둘이 가지고 있는 메모리의 내용(contents)은 같지만, 부모와 자식은 다른 메모리 공간에서 같은 내용(contents)을 가지고 실행되기 때문에 결국 서로 다른 메모리를 가지고 서로 다른 레지스터를 가진다. 따라서 둘 중 한 곳에서 변수의 값을 바꾸더라도 다른 한 곳에 영향을 주지 않는다. 예를 들어, 부모 프로세스에서 wait의 반환값이 pid에 저장될 때, 자식프로세스에서의 pid값은 변하지 않는다. 

exec 시스템 콜은 두 개의 인자를 받는다: 실행 파일(executable)을 포함하는 파일의 이름과 문자열 인자 배열 두 가지를 받는다. exec 시스템 콜은 이 시스템 콜을 호출하는 프로세스의 메모리를 파일 시스템에 저장된 파일에서 로드된 새로운 메모리 이미지로 교체한다. 파일은 반드시 어느 부분이 인스트럭션인지, 어느 부분이 데이터인지, 어느 인스트럭션부터 시작할지 명시하는 특정 포맷(format)이어야 하며, xv6의 경우 ELF format을 사용한다. exec 시스템 콜이 성공하면, 호출한 프로그램에게 아무것도 반환하지 않으며, 파일로 부터 로드된 인스트럭션을 ELF header에 선언된 시작점(entry point)부터 실행한다.

char *argv[3];

argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");

 

위의 코드는 exec을 호출하는 프로그램을 /bin/echo 프로그램의 인스턴스로 바꿔서 실행하며, 이때 인자는 echo hello이다. 관례상 인자 배열의 첫 번째는 프로그램의 이름이므로, 대부분의 프로그램은 이 첫 번째 요소를 무시한다. exec은 에러가 발생할 때만 -1을 반환한다.

xv6 쉘은 사용자를 대신해 프로그램을 실행시킬 때 위의 함수 호출을 사용한다.

xv6는 대부분의 사용자 공간 메모리를 암묵적으로 할당한다. fork는 부모 프로세스의 메모리의 복사본만큼 필요한 메모리를 할당하고, exec은 실행 파일을 담을 수 있을 만큼 큰 메모리를 할당한다. 런타임(run-time)에 더 많은 메모리가 필요한 프로세스는 해당 프로세스의 데이터 메모리를 n 바이트만큼 늘리도록 sbrk(n)을 호출한다. sbrk는 새로운 메모리 공간의 위치를 반환한다.

I/O와 파일 디스크립터(file descriptor)

파일 서술자(file descriptor)는 프로세스가 읽거나 쓸 객체를 나타내는 작은 정수이다. 이 객체는 커널이 관리한다. 프로세스는 (1)파일, 디렉토리, 장치를 열 때 혹은 (2)파이프(pipe)를 생성할 때, (3)기존의 서술자를 복사할 때 파일 서술자를 얻을 수 있다. 간단하게 파일 서술자가 가리키는 객체를 "파일(file)"이라 부른다. 파일 서술자 인터페이스는 결국 파일과, 파이프, 장치 간의 차이점들을 추상화시켜, 이들을 모두 바이트의 스트림(stream)으로 바라볼 수 있도록 만든다. 

내부적으로 xv6 커널은 파일 서술자를 인덱스로 사용해 프로세스마다 테이블을 갖도록 하며, 모든 프로세스는 0부터 시작하는 파일 서술자를 가지고 있다. 관례상 프로세스는 파일 서술자 0(표준 입력)에서 읽고, 파일 서술자 1(표준 출력)에 출력을 쓴다. 그리고 오류 메시지를 파일 서술자 2(표준 에러)에 쓴다. 이러한 관례를 이용해 I/O redirection과 파이프라인을 구현한다. 쉘은 항상 이 세가지 파일 서술자가 열려있도록 하며, 이는 콘솔에 대한 기본적인 파일 서술자이다.

read, write 시스템 콜은 파일 서술자에 의해 식별되는 파일로 부터 바이트를 읽거나 파일에 바이트를 쓴다. read(fd, buf, n)은 파일 서술자 fd에서 최대 n 바이트를 읽어서 buf에 복사하고, 읽은 바이트의 수를 반환한다. 파일을 나타내는 각각의 파일 서술자는 파일과 연관된 오프셋(offset)을 가지고 있는다. read는 현재 파일의 오프셋에서 부터 데이터를 읽고, 오프셋을 읽은 바이트 수만큼 증가시킨다. 

plain text inline code plain textplain text inline code plain textlain text inline code pain text inline code plxt inline code plxt inline code plain text

반응형

댓글