이번 포스팅에서는 지난 번에 이어 C 프로그램에서 system call 함수를 호출하여 process level에서 조작하는 예시를 다룬다.
Wait Child Process: waitpid()
fork() 함수로 새로운 child process를 생성할 수 있고, waitpid() 함수는 child process가 종료될 때까지 기다린다. Argument와 return 값에 대한 자세한 것은 아래 reference를 참고하면 알 수 있는데, 포스팅에서는 이 중 몇 가지만 정리해볼 것이다.
링크: https://www.ibm.com/docs/en/zos/2.4.0?topic=functions-waitpid-wait-specific-child-process-end
waitpid() — Wait for a specific child process to end
Standards Standards / Extensions C or C++ Dependencies POSIX.1 XPG4 XPG4.2 Single UNIX Specification, Version 3 both Format #define _POSIX_SOURCE #include pid_t waitpid(pid_t pid, int *status_ptr, int options); General description Suspends the calling p
www.ibm.com
pid: Determines wait set
함수의 첫 번째 argument는 waitpid() 함수가 기다릴 wait set을 정의한다. 만약 -1인 경우, child process 중 임의의 하나가 종료되기를 기다린다. 0보다 큰 경우, pid는 기다리고자 하는 child process의 process ID를 나타내고, 지정된 하나의 child process의 종료를 기다린다.
status: Check exit status
status는 몇 개의 정의된 함수의 parameter로 넘겨주면 child process의 exit에 대한 정보를 얻을 수 있다. 하나의 예로, WEXITSTATUS(status)는 child process의 exit status를 return한다. 이 외에도 여러 함수가 있는데, 다루지는 않기로 한다.
Return value: pid or -1
만약 성공적으로 child process가 종료되었다면 child process의 process id를 return한다. 그렇지 않은 경우, -1을 return한다.
Example code: reap child process in the order they were created
교재의 Figure 8.19에 소개된 코드를 기반으로 예시 코드를 하나 작성해보았다.
#include <stdio.h>
#include <sys/types.h> // getpid(), fork()
#include <unistd.h> // getpid(), fork()
#include <stdlib.h> // exit()
#include <sys/wait.h> // waitpid()
#define N 3
int main() {
int status, i;
pid_t pid[N], retpid;
for (i=0; i<N; i++){
if ((pid[i] = fork()) == 0) { // Only child process comes here
exit(100+i);
}
}
// Check process id of child processes
for (i=0; i<N; i++){
printf("%d ", pid[i]);
}
printf("\n");
i = 0;
// Reap all child in the order they were created
while ((retpid = waitpid(pid[i++], &status, 0)) > 0) { // retpid is PID of terminated child process
printf("Child %d terminated with exit status %d\n", retpid, WEXITSTATUS(status));
}
printf("\n");
printf("Parent is %d\n", getpid());
exit(0);
}
첫 번째 for 문에서 N개의 child process를 생성한다. Parent process에 대해서는 fork() 함수가 child process의 PID를 return하므로, child process에 대해서만 if 문 안으로 들어와서 종료된다.
이 때, pid라는 배열에 child process의 PID들을 순서대로 저장해둔다.
while loop에서는 맨 처음 child process부터 waitpid() 함수로 종료되기를 기다린다. 만약 성공적으로 종료되었다면 waitpid() 함수의 return 값이 0보다 클 것이고, 이 값은 이전에 pid 배열에 저장되어 있는 PID 값과 같을 것이다.
실행 결과는 아래와 같다.
Load and Run Programs: execve()
execve() 함수는 현재 프로세스에서 새로운 프로그램을 load하고 실행시킨다. 마찬가지로 자세한 내용은 reference 링크를 첨부한다.
링크: https://docs.oracle.com/cd/E19048-01/chorus5/806-7016/6jftugggq/index.html
https://docs.oracle.com/cd/E19048-01/chorus5/806-7016/6jftugggq/index.html
execve(2POSIX) NAME | SYNOPSIS | API RESTRICTIONS | DESCRIPTION | PARAMETERS | RETURN VALUES | ERRORS | EXTENDED DESCRIPTION | ATTRIBUTES | SEE ALSO NAME SYNOPSIS #include int execve(const char *path, char *const argv[], char *const envp[]); API RESTRICTIO
docs.oracle.com
execve(const char *filename, const char *argv[], const char *envp[])
filename은 executable object file의 실행 경로이다. argv에는 프로그램을 실행시키기 위한 argument list, envp는 환경 변수 리스트이다.
이 함수는 오류가 있는 경우만 -1을 return하고, 그렇지 않으면 return하지 않는다.
A Simple Shell Program
fork() 함수와 execve() 함수를 이용하여 간단한 shell 프로그램을 만들 수 있다. Shell은 사용자를 대신하여 다른 프로그램을 실행시켜주는 interactive application-level 프로그램이다.
Main routine
Shell은 사용자로부터 문자열을 입력받고, 이를 evaluate하는 과정을 반복한다. 코드는 아래와 같다.
#include <stdio.h>
#include <sys/types.h> // getpid(), fork()
#include <unistd.h> // getpid(), fork()
#include <stdlib.h> // exit()
#include <sys/wait.h> // waitpid()
#include <string.h> // strcpy
#define MAXLINE 100
#define MAXARGS 10
int main() {
char cmdline[MAXLINE];
while (1) {
printf("> ");
fgets(cmdline, MAXLINE, stdin);
// printf("Entered: %s\n", cmdline);
eval(cmdline);
}
}
fgets() 함수를 통해 사용자 입력을 받는다. fgets() 함수는
char *fgets (char *string, int n, FILE *stream);
읽은 문자의 수가 n-1개가 되거나 줄바꿈 문자('\n')가 등장할 때까지 문자열을 읽어 string에 저장한다. 맨 끝에는 문자열의 끝임을 나타내는 '\0'을 추가한다. 이렇게 읽어들인 문자열이 cmdline에 저장되어 있고, 이를 eval() 함수에 넘겨주어 명령어에 대한 처리를 하도록 한다. (fgets 함수에 대한 자세한 설명은 아래 링크 참고)
링크: https://www.ibm.com/docs/ko/i/7.3?topic=functions-fgets-read-string
fgets() — 스트링 읽기
형식 #include char *fgets (char *string, int n, FILE *stream); 설명 fgets() 함수는 현재 stream 위치에서 어느 것이 먼저 오건 첫 번째 줄 바꾸기 문자(\n)까지, 스트림의 끝까지 또는 읽은 문자 수가 n-1과 같을 때
www.ibm.com
parseline: Split string into char-type array
parseline() 함수는 문자열을 쪼개서 배열로 저장하는데, 이 때 delimiter(=구획 문자, 쪼개는 위치의 기준이 되는 문자)는 공백(' ')이다.
void parseline(char* buf, char** argv) {
//////////////////////////////////////
// Convert string buf into array argv
// Split with space
//////////////////////////////////////
char *delim; // pointer to delimiter(' ')
int argc; // number of args
buf[strlen(buf)-1] = '\0'; // replace newline('\n') with end character('\0')
argc = 0;
while (delim = strchr(buf, ' ')) {
*delim = '\0'; // split buf into sub-string with '\0'
argv[argc++] = buf;
buf = delim + 1; // update buf to point remaining string
// printf("%s\n", buf); // check updated buf
}
argv[argc++] = buf; // last argument
argv[argc] = NULL;
/*
// for loop for checking argv array
for (int i = 0; i < argc; i++) {
printf("%d th: %s\n", i, argv[i]);
}
*/
}
delim이라는 포인터가 반복적으로 문자열 buf의 빈 칸을 가리키도록 하였다. 이는 strchr() 함수를 이용하였는데, 이 함수는 문자열에 대해 특정 문자가 있는 곳을 가리키는 포인터를 return한다. delim을 얻은 후에는, delim이 현재 가리키는 곳의 값을 '\0'으로 변경한다. 그러면 공백을 기준으로 앞 쪽 부분이 하나의 sub-string으로 분리될 수 있다. argv 배열에는 분리된 문자열을 할당할 수 있고, buf는 delim이 가리키는 바로 다음 문자열을 가리키도록 함으로써 반복을 이어갈 수 있다.
eval(): Check if builtin command & Execute program
eval() 함수에서는 우선 built-in command인지를 확인한 뒤, 아니라면 execve() 함수를 이용해 프로그램을 실행시킨다. 여기서는 built-in command는 shell 프로그램을 종료시키는 'quit' 하나만을 고려하기로 한다. 아래와 같이 strcmp() 함수를 이용하여 간단히 구현할 수 있다. built-in command가 아닌 경우는 0을 return한다.
int builtin_command(char **argv){
if (! strcmp(argv[0], "quit")) {
exit(0);
}
return 0;
}
이제 built-in command가 아닌 경우에 대해 프로그램을 실행시키는 부분이다. 이 때, 그냥 execve()만을 호출하게 되면 다시 shell 프로그램으로 돌아오지 못하는데, execve()는 성공적으로 프로그램을 실행시킨 경우 return하지 않기 때문이다.
그래서, 이전 포스팅에서 공부했던 fork() 함수를 이용해 새로운 child process를 만든 뒤, 여기서 execve()를 호출하도록 하였다. Shell 프로그램은 waitpid() 함수를 이용해 child process가 종료되기를 기다리도록 하였다. 그러면 아래와 같이 child process가 성공적으로 종료된 이후에 shell 프로그램의 동작을 이어갈 수 있다.
getpid() 함수를 이용한다면 프로세스의 PID를 출력하여, shell 프로그램에서 다른 프로그램을 실행시킨다는 개념을 이해하는 데에 도움이 될 수 있다. Shell 프로그램에서 출력한 child process의 PID는 실행되는 simple_program.c에서 getpid()로 얻은 PID와 같은 것을 확인할 수 있다.
아래는 eval() 함수의 구현 전체이다.
void eval(char* cmdline) {
char *argv[MAXARGS];
pid_t pid;
int status; // for waitpid()
char buf[MAXLINE];
strcpy(buf, cmdline);
//printf("Entered: %s\n", cmdline);
parseline(buf, argv);
/*
// for loop for checking argv array
for (int i = 0; argv[i] != NULL; i++) {
printf("%d th: %s\n", i, argv[i]);
}
*/
if (! builtin_command(argv)) {
if ((pid = fork()) == 0) { // only for child process
// execute program
if (execve(argv[0], argv, NULL) < 0) {
printf("Execution error.\n");
}
}
if (waitpid(pid, &status, 0) > 0) { // only for parent process(=shell program)
printf("Process with PID %d successfully terminated.\n", pid);
}
}
}
'System Programming > CSAPP Book' 카테고리의 다른 글
Ch 6-2. Locality (2): Matrix Multiplication with Various Computation Order (1) | 2023.05.04 |
---|---|
Ch 6-1. Locality (1): Reference to 2D Array Data (0) | 2023.05.01 |
Ch 8-3. Process Control with C Program: getpid, exit, fork (0) | 2023.04.20 |
Ch 8-2. Process, Private Address Space, Context Switch (0) | 2023.02.23 |
Ch 8-1. Control Flow, Exception Handling, Exception의 종류 (0) | 2023.02.16 |