리눅스 프로그래밍 - 7주차

Lellow_Mellow·2022년 10월 11일
2
post-thumbnail

🔔 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다. 혹여나 틀린 부분이 있다면 지적해주시면 감사드리겠습니다.

exec family

execlp, execvp에서 문자 'p'는 함수가 PATH 환경 변수를 사용하여 실행 파일을 찾는다는 것을 의미한다. 따라서 아래와 같이 PATH에 해당 경로가 위치해있어야 한다.

execl, execle, execlp의 문자 'l'은 함수가 인수를 list로 받는다는 것을 의미하며, execv, execve, execvp의 문자 'v'는 함수가 argv[] vector로 인자를 받는다는 것을 의미한다. execle, execve에서 문자 'e'는 함수가 현재 environment을 사용하는 대신 envp[] 를 사용함을 의미한다.

Example : execl

# include <unistd.h>

int main() {
	printf(“executing ls\n”);
	execl (/bin/ls”, “ls”,-l”, (char*) 0);
	perror (“execl failed to run ls”);
	return 1;
}

execl이므로 list 형태로 인자를 전달하며, 마지막에는 (char*) 0으로 null 혹은 null pointer로 끝을 알린다.

Example : execv

# include <unistd.h>

int main() {
	char *const av[] = {“ls”,-l”, (char *)0};
	printf(“executing ls\n”);
	execv (/bin/ls”, av);
	perror (“execv failed to run ls”);
	return 1;
}

execv이므로 인자와 함게 null까지 vector에 담아 전달해준다.

Example : Accessing argv passed with exec

/* myecho */
int main(int argc, char** argv) {
	while (--argc > 0)
		printf(%s “, *++argv);
	printf(“\n”);
	return 0;
}

/* run_myecho */
#include <unistd.h>

int main() {
	char * const argin[] = {./myecho”, “hello”, “world”, (char *)0};
	execvp (argin[0], argin);
	return 1;
}

// $./run_myecho
// hello world

execvp이므로 argin[0]과 같이 file 이름만 전달하였으며, 해당 file이 위치하는 경로가 PATH에 존재해야한다. myecho에서 전달받을 때, 가장 끝에 있는 (char *)0는 전달되지 않는다.

5.4 Using exec and fork together

단순 fork 만을 사용하면, 한 파일에서 parentchild 의 코드를 전부 포함해야 하므로, exec 와 같이 사용하는 경우가 일반적이다.

이에 대한 간단한 예시 코드를 살펴보겠다.

# include <unistd.h>
	int fatal(char *s) {
	perror(s);
	exit(1);
}

int main() {
	pid_t pid;
	switch (pid = fork()) {
		case -1:
			fatal (“fork failed”);
			return 1;
		case 0:
			execl (/bin/ls”, “ls”,-l”, (char *)0);
			fatal (“exec failed”);
			return 1;
		default:
			wait((int*)0);
			printf(“ls completed\n”);
		return 0;
	}
}

fork 를 수행하여 return값을 pid 에 저장한다.

child의 경우

  • pid = 0
  • ls -l 명령어를 실행

parent의 경우

  • pid > 0
  • wait((int*)0) : child가 종료될때까지 대기
  • 이후 종료

이를 그림으로 나타내면 아래와 같다.

shell에서 option이 없는 command를 실행하는 과정에도 활용될 수 있다.

int docommand(char *command) {
	pid_t pid;
	if ((pid = fork()) < 0)
		return -1;
	if (pid == 0) { /* child */
		execl (/bin/sh”, “sh”,-c”, command, (char *)0);
		perror (“execl”);
		exit(1);
	}
	wait((int *)0);
	return(0);
}

인자로 전달받은 commandfork 를 통해 child 에서 실행한 이후, 종료하는 간단한 함수이다.

5.5 Inherited data and file descriptors

fork 시에 parent의 file descriptor 는 child의 file descriptor 와 동일하다. 가장 상위 process의 file descriptor 에 0, 1, 2번이 기본적으로 포함되어있고, 하위 process들은 이 file descriptor 를 그대로 가져오기 때문에 항상 0, 1, 2번은 기본적으로 가지고 있다.

이에 대한 예시 코드는 아래와 같다.

int printpos(const char *string, int filedes) {
	off_t pos;
	if (( pos = lseek (filedes, 0, SEEK_CUR)) == -1)
		fatal (“lseek failed”);
	printf (%s:%ld\n”, string, pos);
}

int main() {
	int fd;
	pid_t pid;
	char buf[10];
	if ((fd = open(“data”, O_RDONLY)) == -1)
		fatal(“open failed”);
	read(fd, buf, 10);
	printpos(“Before fork”, fd);
	switch (pid = fork()) {
		case -1:
			fatal(“fork failed”);
			break;
		case 0:
			printpos(“Child before read”, fd);
			read(fd, buf, 10);
			printpos(“Child after read”, fd);
		default:
			wait((int *)0);
			printpos(“Parent after wait”, fd);
	}
	return 0;
}

printposlseek 를 이용하여 현재 해당 파일의 file position 을 출력한다.

처음에 파일을 open 한 이후에 10만큼 읽어 buf 에 저장하여 file position 을 10으로 이동시킨다. 이후 fork 하여 child process에서 10만큼 더 읽어 file position 을 20으로 이동시킨다.

이 경우에 childfile position 은 20이며, 동시에 parent 도 동일한 file descriptor table 을 공유하므로 해당 file position 또한 20으로 변경된 것을 알 수 있다.

즉, 서로 동일한 파일을 바라보기 때문에 동일한 position을 공유한다는 의미이다.

부모 process와 자식 process 간 유전되는 정보들

process 자체에 대한 정보보다는 부수적인 data들이 유전된다.

  • real uid, real gid, effective uid, effective gid
  • session id
  • 환경변수들
  • supplementary gids, process gid
    ... 이하 생략

부모 process와 자식 process 간 유전되지 않는 정보들

process와 직접적으로 관련된 data들은 유전되지 않는다.

  • fork부터의 return value
  • pid
  • 부모 pid
  • process 실행 시간
  • File locks, pending alarms, set of pending signal 등 부모가 약속한 것들

exec와 fork의 차이

exec 역시 fork 와 마찬가지로 대부분이 그대로 유지되지만, close-on-exec flag 가 설정된 경우, 해당 file descriptor 는 닫히게 된다.

해당 flag는 default는 0으로 off이지만, 아래 명령어를 통해 flag를 설정해줄 수 있다.

fcntl(fd, F_SETFD, 1);

5.6 Terminating Processes with the exit System Call

process를 정상적으로 종료하는 방식은 5가지, 비정상적으로 종료하는 방법은 3가지가 있다.

Normal termination

  • main으로부터 return
  • exit() 호출
  • _exit(), _Exit() 호출
  • 마지막 thread에서 return (관련된 detail한 방식 2가지)

Abnormal termination

  • abort() 호출
  • thread에서 취소 request
  • signal 수신

System Call : exit, _exit, _Exit

#include <stdlib.h>
void exit(int status);
void _Exit(int status);

#include <unistd.h>
void _exit(int status);

argument

  • int status : 프로그램이 어떻게 종료되었는지를 나타내는 정수, 일종의 프로그램의 유서
  • 하위 8bits만 반영됨, parent에서는 wait() 을 이용해 전달받음.

return

  • 정상 종료일 경우 0
  • 0이 아닐 경우는 문제가 발생한 경우

exit(0)return(0) 와 동일한 역할이며, exit 의 경우 buffer를 모두 비우는 (buffer에 있는 작업을 모두 마무리 이후) clean up processing 이후 종료하며, _exit(), _Exit()은 바로 종료하게 된다는 차이점이 존재한다.

기본적으로 exec 실행 시, start-up routine 을 거쳐 main 이 실행되는데, 명시적으로 _exit, _Exit 을 호출하지 않는 이상 항상 exit 을 호출하게 된다.

exit 과정에서 exit handler프로그램이 종료될 때 수행해야 할 함수들을 가리키는 역할을 하며, 이는 아래의 function으로 지정이 가능하다.

Function : atexit

#include <stdlib.h>
int atexit(void (*func)(void));

argument

  • void (*func)(void) : 실행할 함수

return

  • 성공할 경우 0 return
  • error 발생 시, 0이 아닌 값 return

일반적으로 32개의 function을 등록할 수 있다.

이에 대한 예시는 아래와 같다.

void func1() { printf(“print func1\n”); }
void func2() { printf(“print func2\n”); }
void func3() { printf(“print func3\n”); }
void func4() { printf(“print func4\n”); }

int main() {
	pid_t pid;
	atexit(func1);
	atexit(func2);
	atexit(func3);
	atexit(func4);
	if ((pid = fork()) < 0) {
		perror(“fork failed”);
		exit(1);
	}
	if (pid == 0) {
		printf(“child process is called\n”);
		printf(“child process calls exit\n”);
		exit(0);
	}
	wait(NULL);
	printf(“parent process calls exit\n”);
	exit(0);
}

atexit 으로 등록해준 역순으로 실행되며, handler 역시 자식에게 유전되는 정보 중 하나이다. 따라서 parent에서 등록하여도 child에서 동일하게 실행된다.

5.7 Synchronizing Process

process가 종료되면, kernel 이 열린 모든 file descriptor 를 닫아주고, 사용된 memory 를 release 해준다. 하지만 위에서 유서라 칭했던 exit status, PID, CPU time 은 남아있다. 이를 parent에서 wait 을 통해 처리해줘야 한다.

System call : wait

#include <sys/wait.h>
pid_t wait(int *statloc);

argument

  • int *statloc : 상위 16bits는 0, 그 다음 8bits는 exit number, 하위 8bits는 다른 정보가 담김. childexit status를 가리키며, NULL일 경우, 무시됨.

return

  • 성공할 경우 child의 PID를 return
  • error 발생 시 -1 return : errno = ECHILD 로 자식이 없음을 의미
  • 종료 신호와 함께 0 return

기본적으로 여러명의 자식이 존재하면, 가장 먼저 종료되는 자식을 기준으로 return 된다.

또한 2가지 macro를 사용해 아래의 경우를 확인할 수 있다.

  • WIFEXITED : exit status가 0이 아니면 1 return, 아니면 0 return (즉, 중간 8bit에 해당하는 exit number를 확인하는 것. 0이면 정상 종료를 의미)
  • WEXITSTATUS : exit status return (8bit 오른쪽으로 shift 이후, 하위 8bit return)

이에 대한 예시 코드는 아래와 같다.

#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main() {
	pid_t pid;
	int status, exit_status;
	if ((pid=fork()) < 0)
		fatal (“fork failed”);
	if (pid == 0) {
		sleep(4);
		exit(5);
	}
	if ((pid = wait(&status)) == -1) {
		perror(“wait failed”);
		exit(2);
	}
	if (WIFEXITED(status)) {
		exit_status = WEXITSTATUS(status);
		printf(“Exit status from %d was %d\n”, pid, exit_status);
	}
	exit(0);
}

fork 하여 return 받은 pid가 0보다 작다면 error 에 해당한다. pid가 0일 경우, 4초동안 sleep 후, exit(5) 를 실행한다. parent는 wait(&status) 를 통해 status에 exit status를 저장한다. 이를 WIFEXITED macro를 이용해 0이 아님을 판단하고, 0이 아닐 경우에는 WEXITSTATUS macro를 이용하여 하위 8bit, 즉 실질적인 exit number인 5를 출력하게 된다.

System Call : waitpid

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *sataloc, int options);

argument

  • pid_t pid
    - == -1 : 어떤 child인지 상관 없음 (wait과 동일한 역할)
    - > 0 : PID가 pid와 동일한 child를 wait
    - == 0 : PGID가 calling process와 동일한 child를 wait
    - < 0 : PGID가 pid의 절대값과 같은 child를 wait?
  • int *statloc : wait과 동일
  • int options
    - WCONTINUED : 정지 및 재개된 child process의 status를 전달받음
    - WNOHANG : 종료된 child가 없을 경우, 즉시 0을 반환하며 block하지 않음
    - WUNTRACED : 종료된 child process의 status를 전달받음

return (wait의 return과 동일)

  • 성공할 경우 child의 PID를 return
  • error 발생 시 -1 return : errno = ECHLID 로 자식이 없음을 의미
  • 종료 신호와 함께 0 return

이에 대한 예시는 아래와 같다.

#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

main() {
	pid_t pid;
	int status, exit_status;
	if ((pid=fork()) < 0)
		fatal (“fork failed”);
	if (pid == 0) {
		sleep(4);
		exit(5);
	}
	while ((pid = waitpid(pid, &status, WNOHANG)) == 0) {
		printf(“Still waiting...\n”);
		sleep(1);
	}
	if (WIFEXITED(status)) {
		exit_status = WEXITSTATUS(status);
		printf(“Exit status from %d was %d\n”, pid, exit_status);
	}
	exit(0);
}

wait의 예제와 비슷한 결과를 보여주는 코드이나, waitpidWNOHANG option의 경우, **이전에 종료된 child process가 존재하면 해당 pid를 return, 없다면 바로 0을 return한다. 0일 경우, 1초 sleep 이후 다시 waitpid를 실행하는 코드이다. 출력 결과는 동일하게 5를 출력한다.

Compare wait & waitpid

wait은 child process가 종료될때까지 block되는 것에 비해, waitpidWNOHANG option을 사용하면 non blocking으로 수행이 가능하다.

5.8 Zombie and premature exits

Zombie Process

parent process에서 wait 을 호출하지 않아 완전히 종료되지 않은 child process를 Zombie process 라고 한다. 실행이 완료되었지만, 여전히 process table에 남아있는 상태가 된다.

위 그림이 Zombie process 의 예시이다.

Orphan Process

parent process가 child process보다 먼저 종료된 경우, child process는 Orphan process가 된다. 해당 process의 parent는 init process (pid = 1)가 되며, 이는 주기적으로 wait을 호출하므로, Zombie process로 남지는 않는다.

위의 그림이 그 예시이며, Orphan process 역시 init에서 wait을 하기 전까지는 Zombie process가 된다.

모든 process는 Zombie process인 기간이 존재한다.

해당 주차에 추가로 학습한 command

  • echo $?
    직전에 종료한 process의 return값을 echo

execvp, execlp, execv

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char* argv[]) {
	pid_t pid;
	if (argc < 2) {
		printf("Usage: %s PROG [PROG_ARG]...\n", argv[0]);
        return 1;
	}
	pid = fork();
	argv[argc] = NULL;
	if (pid == 0) {
		execvp(argv[1], &argv[1]);
		perror("execv failed");
		return 1;
	}
	else if (pid < 0) {
		perror("fork failed");
		return 2;
	}
	wait(NULL);
	return 0;
}

위 코드에서는 execvp를 사용하였다. 이 대신 execlp를 사용할 경우, 이후 인자가 몇개가 나열될지 예상이 불가능하므로, vector로 전달이 가능한 execvp를 사용하는 것이 적절하다.

또한 execv 역시 명령어를 실행하는 것이므로, 이름으로 전달받을 수 있는 execvp를 사용하는 것이 적합하다.

wait(NULL)

wait(NULL)은 parent process에서 child process의 작업이 종료될때까지 기다리는 역할을 한다. 해당 코드가 없을 경우, child가 종료되고 parent가 종료되는 것이 아니라, 그 반대가 될 수도 있으므로 상황에 맞게 사용하는 것이 중요하다.

profile
festina lenta

1개의 댓글

comment-user-thumbnail
2022년 10월 18일

시프를 듣기전에 이 글을 봤었다면... 라는 생각이 드네요. 잘 읽었습니다.

답글 달기