xv6-book (Ch1.1-Processes and memory)
프로세스 & 메모리
xv6 프로세스는 사용자 공간 메모리(명령어, 데이터 및 스택)와 커널에 대한 프로세스별 개인 상태로 구성됩니다. xv6는 프로세스 시간을 공유하며, 대기 중인 프로세스 집합 사이에서 사용 가능한 CPU를 투명하게 전환합니다.
프로세스가 실행되지 않을 때, xv6는 CPU 레지스터를 저장하고, 프로세스를 다시 실행할 때 복원합니다. 커널은 각 프로세스에 프로세스 식별자 또는 PID를 할당합니다.
프로세스는 fork 시스템 호출을 사용하여 새로운 프로세스를 생성할 수 있습니다. Fork는 호출한 프로세스인 부모 프로세스와 정확히 같은 메모리 내용을 가진 새로운 프로세스, 즉 자식 프로세스를 생성합니다. Fork는 부모와 자식 모두에서 반환됩니다. 부모에서는 fork가 자식의 PID를 반환하고, 자식에서는 fork가 0을 반환합니다.
다음은 C 프로그래밍 언어로 작성된 프로그램입니다.
int pid = fork();
if(pid > 0) {
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid;
} else if(pid == 0) {
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}
아래는 System call과 설명입니다.
System call | Description |
int fork() | 프로세스를 생성하고, 자식의 PID를 반환합니다. |
int exit(int status) | 현재 프로세스를 종료하고, 상태는 wait()에 보고됩니다. 반환값이 없습니다. |
int wait(int *status) | 자식 프로세스가 종료될 때까지 기다리고, 종료 상태를 *status에 저장하며, 자식 프로세스의 PID를 반환합니다. |
int kill(int pid) | 프로세스 PID를 종료합니다. 오류가 없으면 0을 반환하고, 오류가 발생하면 -1을 반환합니다. |
int getpid() | 현재 프로세스의 PID를 반환합니다. |
int sleep(int n) | n clock 틱 동안 일시 정지합니다. |
int exec(char *file, char *argv[]) | 파일을 로드하고 인자와 함께 실행합니다. 오류가 발생한 경우에만 반환합니다. |
char *sbrk(int n) | 프로세스의 메모리를 n 바이트만큼 확장합니다. 새로운 메모리의 시작 위치를 반환합니다. |
int open(char *file, int flags) | 파일을 엽니다. 플래그는 읽기/쓰기 여부를 나타냅니다. 파일 디스크립터(fd)를 반환합니다. |
int write(int fd, char *buf, int n) | 버퍼(buf)에서 파일 디스크립터(fd)로부터 n 바이트를 씁니다. n을 반환합니다. |
int read(int fd, char *buf, int n) | n 바이트를 버퍼(buf)로 읽어들입니다. 읽은 바이트 수를 반환하며, 파일의 끝에 도달한 경우 0을 반환합니다. |
int close(int fd) | 열린 파일(fd)을 해제합니다. |
int dup(int fd) | fd와 동일한 파일을 가리키는 새로운 파일 디스크립터를 반환합니다. |
int pipe(int p[]) | 파이프를 생성하고, 읽기/쓰기 파일 디스크립터를 p[0]과 p[1]에 넣습니다. |
int chdir(char *dir) | 현재 디렉토리를 변경합니다. |
int mkdir(char *dir) | 새로운 디렉토리를 생성합니다. |
int mknod(char *file, int, int) | 장치 파일을 생성합니다. |
int fstat(int fd, struct stat *st) | 열린 파일에 대한 정보를 *st에 넣습니다. |
int stat(char *file, struct stat *st) | 이름이 지정된 파일에 대한 정보를 *st에 넣습니다. |
int link(char *file1, char *file2) | 파일 file1에 대한 다른 이름(file2)을 생성합니다. |
int unlink(char *file) | 파일을 삭제합니다. |
시스템 호출들은 오류가 없을 때 0을 반환하고, 오류가 있을 때 -1을 반환합니다.
exit 시스템 호출은 호출한 프로세스가 실행을 중지하고 메모리 및 열린 파일과 같은 리소스를 해제합니다. exit는 정수 상태 인자를 취하며, 전통적으로 성공을 나타내는 0과 실패를 나타내는 1을 사용합니다.
wait 시스템 호출은 현재 프로세스의 종료된 자식의 PID를 반환하고, wait에 전달된 주소에 자식의 종료 상태를 복사합니다. 호출자의 자식 중 아무도 종료되지 않은 경우, wait는 종료될 때까지 기다립니다. 호출자에게 자식이 없으면, wait는 즉시 -1을 반환합니다. 부모가 자식의 종료 상태에 대해 관심이 없는 경우, wait에 0 주소를 전달할 수 있습니다.
parent: child=1234
child: exiting
printf 호출에 도달하는 것이 부모인지 아니면 자식인지에 따라 두 경우 모두 발생할 수 있습니다. 자식이 종료된 후에, 부모의 wait가 반환되면 부모가 출력을 수행하게 됩니다.
parent: child 1234 is done
비록 자식 프로세스가 초기에 부모와 동일한 메모리 내용을 가지고 있지만, 부모와 자식은 서로 다른 메모리와 레지스터를 사용하여 실행됩니다. 따라서 한 쪽에서 변수를 변경해도 다른 쪽에는 영향을 미치지 않습니다.
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
이 프래그먼트는 호출하는 프로그램을 인수 목록이 echo hello인 /bin/echo 프로그램의 인스턴스로 교체합니다. 대부분의 프로그램은 인수 배열의 첫 번째 요소를 무시하며, 이는 일반적으로 프로그램의 이름입니다.
위의 호출을 사용하여 xv6 셸은 사용자를 대신하여 프로그램을 실행합니다. 셸의 주요 구조는 간단합니다. 메인 루프는 사용자로부터 입력을 받아 getcmd로 한 줄씩 읽습니다. 그런 다음 fork를 호출하여 셸 프로세스의 복사본을 생성합니다. 부모 프로세스는 wait를 호출하는 반면 자식 프로세스는 명령을 실행합니다. 예를 들어, 사용자가 셸에 "echo hello"를 입력했다면, runcmd가 "echo hello"를 인자로하여 호출될 것입니다. runcmd는 실제 명령을 실행합니다. "echo hello"의 경우 exec가 호출됩니다. exec가 성공하면 자식 프로세스는 runcmd가 아니라 echo에서 명령을 실행합니다. 어느 시점에서 echo가 exit를 호출하면, 이로 인해 부모는 main에서 wait를 반환하게 됩니다.
"fork"와 "exec"가 왜 하나의 호출로 결합되지 않았는지 궁금할 수 있습니다. 나중에 셸이 I/O 리다이렉션의 구현에서 이 분리를 활용한다는 것을 알게 될 것입니다. 중복 프로세스를 생성하고 즉시 (exec로) 대체하는 것의 낭비를 피하기 위해, 운영 커널은 "fork"를 이러한 사용 사례에 대한 최적화를 통해 가상 메모리 기술인 복사 시 쓰기(copy-on-write)와 같은 기술로 구현합니다.
xv6는 대부분의 사용자 공간 메모리를 암시적으로 할당합니다. fork는 부모의 메모리 복사본을 위해 필요한 메모리를 할당하고, exec는 실행 파일을 보관할 충분한 메모리를 할당합니다. 실행 중에 더 많은 메모리가 필요한 프로세스(아마도 malloc을 위해)는 sbrk(n)을 호출하여 데이터 메모리를 n 바이트만큼 확장할 수 있습니다. sbrk는 새로운 메모리의 위치를 반환합니다.
transparently : 투명하게
available : 사용 가능