[Linux] 프로세스 생성과 실행

프로세스 ID

: 각 프로세스에 대한 식별 번호

#include <unistd.h>

int getpid(void);
// 현재 프로세스의 ID 반환

int getppid(void);
// 현재 프로세스의 부모 프로세스 ID 반환

 

실습: pid.c

#include <stdio.h>
#include <unistd.h>

int main(void)
{
	printf("pid = %d\n", getpid()); /* pid 가져오기 */
	printf("ppid = %d\n", getppid()); /* ppid 가져오기 */

	return 0;
}

 

자식 프로세스의 생성 - fork() 시스템 호출 함수

프로세스의 생성: fork()

: 자신을 복제한 자식 프로세스를 새로 생성

  • 새 프로세스를 생성하는 유일한 방법
  • 부모 프로세스자식 프로세스 ID 반환
  • 생성된 자식 프로세스0 반환
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

 

프로세스 생성 전후

부모 프로세스는 pid 값으로 '자식 프로세스의 pid값'을 가지고, 자식 프로세스는 0을 가진다.

 

실습: fork1.c, fork2.c, fork3.c

fork1.c

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

int main(void)
{
	printf("[%d] process: start\n", getpid());

	pid_t pid = fork();/* 프로세스 생성 */
	printf("[%d] process: return value %d\n", getpid(), pid);

	printf("[%d] process: end\n", getpid());

	return 0;
}

 

fork2.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
	printf("[%d] process: start\n", getpid());

	pid_t pid = fork();
	/* child */ 
	if (pid == 0){
		printf("Hello, child process: %d\n", getpid());
		exit(0);
	}
	/* parent */ 
	else if (pid > 0) {
		printf("Hello, parent process: %d\n", getpid());
	}

	printf("[%d] process: end\n", getpid());

	return 0;
}

 

fork3.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
	printf("[%d] process: start\n", getpid());

	pid_t pid1 = fork();
	if (pid1 == 0) {
		printf("Hello, child1 process: %d\n", getpid());
		exit(0);
	}

	pid_t pid2 = fork();
	if (pid2 == 0) {
		printf("Hello, child2 process: %d\n", getpid());
		exit(0);
	}

	printf("Hello, parent process: %d\n", getpid());

	printf("[%d] process: end\n", getpid());

	return 0;
}

 

자식 프로세스와 동기화: wait()

: 자식 프로세스 중 하나가 종료할 때까지 대기

  • 반환값: 종료된 자식 프로세스의 id
  • status를 통해 자식 프로세스의 종료 코드를 전달받음 (POSIX 표준에서 0~255의 1바이트 데이터: status >> 8 == WEXITSTATUS(status))
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);
// options : 보통 0 사용으로 자식이 종료될 때까지 부모 대기

 

실습: wait.c, waitpid.c

wait.c

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

int main(void)
{
	printf("[%d] process: start\n", getpid());

	pid_t pid = fork();
	if (pid == 0) {
		printf("Hello, child process: %d\n", getpid());
		exit(123); // bbbB
	}

	int status;
	int child = wait(&status); /* 자식이 종료되기를 대기 */ 
	printf("[%d] child process end: %d\n", getpid(), child);
	/* 종료 코드값 얻기: bbBb -> 0bbB -> B
	=> (char)(status >> 8) == WEXITSTATUS(status) */
	printf("\tend code: %d\n", WEXITSTATUS(status)); 

	return 0;
}

 

waitpid.c

// P1이 종료할 때까지 대기
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
	printf("[%d] process: 시작\n", getpid());

	pid_t pid1 = fork();
	if (pid1 == 0) {
		printf("Hello, child1 process: %d\n", getpid());
		sleep(3);
		printf("bye, child1 process: %d\n", getpid());

		exit(1);
	}

	pid_t pid2 = fork();
	if (pid2 == 0) {
		printf("Hello, child2 process: %d\n", getpid());
		sleep(2);
		printf("bye, child2 process: %d\n", getpid());

		exit(2);
	}

	int status;
	int child = waitpid(pid1, &status, 0); /* pid1이 종료하기를 대기 */
	printf("[%d] child1 process 종료: %d\n", getpid(), child);
	printf("\t종료 코드: %d\n", WEXITSTATUS(status));	// status >> 8

	return 0;
}

 

새 프로세스의 실행 - exec() 시스템 호출 함수

새 프로그램 실행: exec()

: 현재 프로세스의 내용을 새 프로그램으로 대체

  • 새 프로그램의 main()이 실행됨
  • pid는 바뀌지 않음

 

#include <unistd.h>

int execl(char* path, char* arg0, char* arg1, ... , char* argn, NULL)
int execv(char* path, char* argv[])
int execlp(char* file, char* arg0, char* arg1, ... , char* argn, NULL)
int execvp(char* file, char* argv[])
  •  path가 나타내는 새 프로그램의 코드/데이터/힘/스택 등으로 대체 후 처음부터 실행
    • ~l : 인자를 개별적으로 전달
    • ~v : 인자를 배열로 전달
    • ~p : 실행파일의 경로명을 PATH 상에서 찾음
  • 반환값: 성공시 반환되지 못함. 실패시 -1 반환

 

실습: exec1_1.c, exec1_2.c, exec2.c, exec3.c

exec1_1.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	printf("executing ls\n");

	/* "/bin/ls"을 인자 목록 "ls", "-l"를 가지고 실행 */
	execl("/bin/ls", "ls", "-F", "/", NULL) // == "ls-F /", 인수의 개수가 가변적이므로 마지막에 NULL을 넣어야 정상적으로 처리됨
	perror("execl failed to run ls");
	printf("end");

	return 1;
}

 

exec1_2.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	printf("executing ls\n");

 	char* av[] = { "ls", "-l", NULL };

	/* "/bin/ls"을 인자 목록 av를 가지고 실행 */
	execv("/bin/ls", av)
	perror("execl failed to run ls");
	printf("end");

	return 1;
}

 

exex2.c

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

int fatal(char* s);

int main()
{
	printf("executing ls\n");

	pid_t pid;
	/* 자식 프로세스 생성 */
	switch (pid = fork()) {
	/* 오류 발생하면 */
	case -1:
		fatal("fork failed");
		break;

	/* 자식이면 */
	case 0:
		execl("/bin/ls", "ls", "-F", NULL);
		fatal("exec failed");
		break;

	/* 부모이면 */
	default:
		wait(NULL);
		printf("ls completed\n");
	}

	return 0;
}

int fatal(char* s)
{
	perror(s);
	exit(1);
}

 

exec3.c

exec3.c의 명령행 인자

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

int main(int argc, char* argv[]) // ./exec3 ls -F /
{
	pid_t pid = fork();
	// 자식 프로세스이면..
	if (pid == 0) {
		/* 명령행 인자 1번 이후의 인자로 실행 */
		execvp(argv[1], &argv[1]);
		fprintf(stderr, "%s: 실행 불가\n", argv[1]);
	}
	// 부모 프로세스라면..
	else {
		int status;
		int child = wait(&status);/* 자식이 끝나기를 대기 */
		printf("[%d] child process end: %d\n", getpid(), pid);
		printf("\texit code: %d\n", WEXITSTATUS(status));
	}

	return 0;
}

명령어 실행 호출: system()

: 내부적으로 자식 프로세스를 생성하고 /bin/sh을 이용해 지정된 명령어 실행

#include <stdlib.h>

int system(const char* cmdstring);
  • /bin/sh -c cmdstring를 호출하여 cmdstring으로 지정된 명령어를 실행 : 내부적으로 fork(), exec(), waitpid() 시스템 호출 이용
  • 명령어 실행이 끝난 후 명령어의 종료코드 반환
    • -1 with errno: fork()나 waitpid() 실패시
    • 127: exec() 실패 시

 

실습: syscall.c, mysystem.c

syscall.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>	// WEXITSTATUS()

int main(void)
{
	int status;
	/* date를 실행 */
	if ((status = system("date"))) < 0)
		perror("system() error");
	printf("\t종료 코드: %d\n", WEXITSTATUS(status));

	/* hello를 실행 */
	if ((status = system("hello")) < 0)
		perror("system() error");
	printf("\t종료 코드: %d\n", WEXITSTATUS(status));

	/* who; exit 44를 실행 */
	if ((status = system("who; exit 44")) < 0)
		perror("system() error");
	printf("\t종료 코드: %d\n", WEXITSTATUS(status));

	return 0;
}

 

mysystem.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>	// EINTR

int mysystem(const char* cmdstring); //./mysystem pwd

int main(int argc, char* argv[])
{
	if (argc == 2)
		mysystem(argv[1]);

	return 0;
}

int mysystem(const char* cmdstring)
{
	if (cmdstring == NULL)
		return 1;

	pid_t pid = fork();
	if (pid == -1)
		return -1;

	if (pid == 0) {
		/* /bin/sh -c cmdstring 형태로 실행 */
		execl("/bin/sh", "sh", "-c", cmdstring, NULL);
		_exit(127);	/* 명령어 실행 오류 */
	}

	do {
		int status;
		/* pid번 자식이 끝날 때까지 대기 */
		if (waidpid(pid, &status, 0) == -1) {
			if (errno != EINTR)	// Error INTeRrupt
				return -1;
		}
		else {
			return status;
		}
	} while (1);
}

프로그램의 종료 처리

프로그램의 종료 : 정상 종료(normal termination)

: main에서 return 시 C 시작 루틴은 그 반환값을 가지고 exit() 호출

 - 프로그램 내에서 exit 호출

 - 프로그램 내에서 _exit 호출

 

프로그램의 종료: 비정상 종료(abnormal termination)

 - 실행 중 시그널을 받아 종료

 - abort() 호출: 프로세스에 SIGABRT 시그널을 보내 종료

 

 

정상 종료 처리 과정

1. 모든 열린 스트림을 닫고, 출력 버퍼의 내용을 쓰는 등의 뒷정리 수행 : exit()와 달리, _exit()는 이 과정 생략

2. 프로세스를 정상적으로 종료

3. 종료 코드를 부모 프로세스에게 전달

#include <stdlib.h>
void exit(int status);
// 뒷정리를 한 후 프로세스를 정상적으로 종료

#include <unistd.h>
void _exit(int status)
// 뒷정리를 하지 않고 프로세스를 즉시 종료

 

프로그램의 시작과 종료 과정

 

exit 핸들러

: 프로세스 정상 종료시 사용자가 지정된 별도의 뒷정리 작업 수행 위해 등록

: 프로세스 당 32개까지 등록 가능 - exit()는 exit 핸들러를 등록 역순으로 호출

 - 매개변수 func: void f(void); 유형의 함수에 대한 포인터

#include <stdlib.h>

int atexit(void (*func)(void));
// 반환값: 0 if OK, nonzero on error

 

실습 : atexit.c

atexit.c

#include <stdio.h>
#include <stdlib.h>

static void exit_handler1(void);
static void exit_handler2(void);

int main(void)
{
	if (atexit(exit_handler1) != 0)
		perror("exit_handler1을 등록할 수 없음!");

	if (atexit(exit_handler2) != 0)
		perror("exit_handler2를 등록할 수 없음!");

	printf("main() 끝\n");

	exit(0);
}

static void exit_handler1(void)
{
	printf("1st exit handler\n");
}

static void exit_handler2(void)
{
	printf("2nd exit handler\n");
}

(+) _exit()를 쓰는 경우, 라이브러리 함수가 아니므로 오류가 발생한다. 따라서 코드 상단에 #include <unistd.h>를 추가해야 한다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void exit_handler1(void);
static void exit_handler2(void);

int main(void)
{
	if (atexit(exit_handler1) != 0)
		perror("exit_handler1을 등록할 수 없음!");

	if (atexit(exit_handler2) != 0)
		perror("exit_handler2를 등록할 수 없음!");

	printf("main() 끝\n");

	_exit(0); 
}

static void exit_handler1(void)
{
	printf("1st exit handler\n");
}

static void exit_handler2(void)
{
	printf("2nd exit handler\n");
}