본문 바로가기

System Programming/CSAPP Book

Ch 8-4. Process Control with C Program(2): waitpid, execve, A Simple Shell Program

이번 포스팅에서는 지난 번에 이어 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);
		}
	}
}