본문 바로가기
Computer Science/UNIX & Linux

[UNIX/Linux] ep7) 프로세스 생성과 실행

by 클레어몬트 2024. 10. 21.

(복습)
ㅁUNIX 계열의 프로세스 생성 기법 - fork와 exec로 복제와 옷 갈아입기
- 부모 프로세스: fork()
자신의 복사본을 자식 프로세스로 생성(복제)
- 자식 프로세스: exec()
자신의 메모리 공간을 새로운 프로그램으로 덮어씀(옷 갈아입기)
※ fork를 통해 복사된 자식 프로세스도 fork를 할 수 있다
https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process

[운영체제] ep2) 프로세스(process)

ㅁ프로세스(process): 실행 중인 프로그램 (실행/스케줄링의 단위 및 자료구조)보조기억장치에 저장된 프로그램을 메모리에 적재하고 실행하는 순간, 그 프로그램은 프로세스가 된다그리고 이 과

claremont.tistory.com

 
 

프로세스 생성 함수: fork(), vfork()

[프로그램 실행 함수]
 int system(const char* command): 프로그램 실행
- command : 실행할 명령이나 실행 파일명
기존 명령이나 실행 파일명을 인자로 받아 셸에 전달한다
셸은 내부적으로 새 프로세스를 생성해 인자로 받은 명령을 실행한다
해당 명령의 실행이 끝날 때까지 기다렸다가 종료 상태를 반환한다

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

int main() {
    int ret;

    // 파이프로 연결된 ps -ef | grep sshd > sshd.txt 명령을 실행하도록 system() 함수를 호출
    // 인자로 전달된 명령은 현재 실행 중인 프로세스에서 ‘sshd’를 포함한 내용을 찾아 sshd.txt 파일에 저장
    ret = system("ps -ef | grep sshd > sshd.txt");
    printf("Return Value : %d\n", ret);

    return 0;
}
/*
실행 결과: cat 명령을 사용해 han.txt 파일의 내용을 살펴보면
system() 함수로 실행한 명령인 sh -C ps -ef | grep sshd > sshd.txt가 실행되고 있는것을 확인
system() 함수로 명령을 실행하면 본 셸로 실행되고 -C 옵션이 지정
본 셸에서 –C 옵션은 문자열에서 명령을 읽어오라는 뜻
*/

 
 
 
ㅁfork() 함수의 개요
Linux에서 프로세스를 생성해 프로그램을 실행하는 대표적인 방법은 fork() 함수를 사용하는 것이다
- 자식 프로세스: fork() 함수가 생성한 부모 프로세스와 똑같은 새로운 프로세스
 
fork() 함수가 리턴하면 부모 프로세스와 자식 프로세스가 동시에 동작하는데, 어느 프로세스가 먼저 실행될지는 알 수 없다
따라서 우리는 프로세스 "동기화" 함수를 사용하여야 한다
(처리 순서는 시스템의 스케줄링에 따라 달라진다)

번호 순서를 잘 보자

fork() 함수를 호출
새로운 프로세스(자식 프로세스)를 생성
fork() 함수로 생성한 자식 프로세스의 메모리 공간은 부모 프로세스의 메모리 공간을 그대로 복사해서 만듦
fork() 함수는 부모 프로세스에는 자식 프로세스의 PID를 리턴하고 자식 프로세스에는 0을 리턴
 
[자식 프로세스가 상속받는 대표적인 속성]
• 실제 사용자 ID(RUID), 유효 사용자 ID(EUID), 실제 그룹 ID(RGID), 유효 그룹 ID(EGID)
• 환경 변수
• 열린 파일 기술자(fd)
• 시그널 처리 설정
• setuid, setgid 설정
• 현재 작업 디렉터리
• umask 설정값
• 사용 가능한 자원 제한
 
[자식 프로세스와 부모 프로세스와 다른 점]
• 자식 프로세스는 새로 할당된 프로세스 ID를 가짐
• 즉, 자식 프로세스를 호출한 부모 프로세스가 자식 프로세스의 PPID로 설정
• 자식 프로세스는 부모 프로세스가 연 파일 기술자(fd)에 대한 복사본을 갖고 있음
(부모 프로세스와 자식 프로세스가 같은 파일의 오프셋을 공유하고 있는 상태가 되므로 읽거나 쓸 때 주의해야 함)
• 자식 프로세스는 부모 프로세스가 설정한 프로세스 잠금, 파일 잠금, 기타 메모리 잠금 등은 상속하지 않음
• 처리되지 않은 시그널은 자식 프로세스로 상속되지 않음
• 자식 프로세스의 tms 구조체 값은 0으로 초기화, 즉 프로세스 실행 시간을 측정하는 기준 값이 새로 설정
 
 
ㅇ pid_t fork(void): 프로세스 생성
- command : 실행할 명령이나 실행 파일명
성공하면 부모 프로세스에는 자식 프로세스의 PID를 / 자식 프로세스에는 0을 반환, 실패하면 –1을 반환
(인자를 따로 받지 않는다)

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

int main() {
    pid_t pid;

    switch (pid = fork()) { // fork() 함수 호출
        case -1 : /* 오류가 발생한 경우 */
            perror("fork");
            exit(1);
            break;
        case 0 : /* child process에 리턴한 경우 */
            printf ("Child Process - My PID:%d, My Parent's PID:%d\n", (int)getpid(), (int)getppid());
            break;
        default : /* parent process에 리턴한 경우 */
            printf("Parent process - My PID:%d, My Parent's PID:%d, My Child's PID:%d\n", (int)getpid(), (int)getppid(), (int)pid);
            break;
    }

    printf("End of fork\n"); // 자식 프로세스, 부모 프로세스 각각 실행하므로 두 번 출력

    return 0;
}
/*
실행 결과: 자식 프로세스에서 출력한 부모 프로세스의 PID와 부모 프로세스에서 출력한 자신의 PID가 동일함을 알 수 있음
또한 부모 프로세스는 fork() 함수의 리턴값으로 자식 프로세스의 PID를 받는데,
이를 출력하면 자식 프로세스가 출력한 PID와 같음을 알 수 있음
(이번 실행 결과에서는 부모 프로세스가 먼저 실행되었지만 다시 실행했을 때는 자식 프로세스가 먼저 실행될 수도 있음)
*/

 
 
ㅁ프로세스 종료 함수: exit() 등
Linux는 프로세스가 종료되면 해당 프로세스가 어떻게 종료되었는지를 나타내는 종료 상태를 저장한다

 
• 자식 프로세스는 부모 프로세스에 자신이 어떻게 종료되었는지를 알리는 종료 상태값을 리턴할 수 있음
• 일반적으로 종료 상탯값이 0이면 정상적으로 종료했음을 의미하고, 0이 아니면 오류가 발생했음을 의미
 
 
ㅇ void exit(int status): 프로그램 종료
- status : 종료 상태값
프로세스를 종료시키고 부모 프로세스에 종료 상태값을 전달
 
• exit() 함수는 프로세스가 사용 중이던 모든 표준 입출력 스트림에 데이터가 남아 있으면 이를 모두 기록하고 열려 있는
스트림을 모두 닫음
• 그 다음 tmpfile() 함수로 생성한 임시 파일을 모두 삭제하고 _exit() 함수를 호출
• _exit() 함수는 시스템 호출인데 프로세스가 사용하던 모든 자원을 반납
 
※ exit() 함수는 atexit() 함수로 예약한 함수를 지정된 순서와 역순으로 모두 실행한다
(atexit() 함수로 예약한 함수가 수행 도중에 문제가 발생해 리턴하지 못하면 exit() 함수의 나머지 과정도 수행되지 않음)
 
ㅇ void _exit(int status): 프로그램 종료
프로그램에서 직접 사용하지 않고 exit() 함수 내부에서 호출
 
호출로 프로세스를 종료할 때 다음과 같은 과정을 통해 시스템 관련 자원을 정리
① 모든 파일 기술자(fd)를 닫음
② 부모 프로세스에 종료 상태를 알림
③ 자식 프로세스에 SIGHUP 시그널 전송
④ 부모 프로세스에 SIGCHLD 시그널 전송
⑤ 프로세스 간 통신(IPC)에 사용한 자원을 반납
 
 
 
exec는 execute의 약자
ㅁexec 함수군: 새로운 프로그램을 현재 프로세스의 메모리에 로드하고 실행 - 옷 갈아입기
exec 함수군은 인자로 받은 다른 프로그램을 자신을 호출한 프로세스의 메모리에 덮어쓴다. 따라서 프로세스가 수행 중이던 기존 프로그램은 중지되어 없어지고 새로 덮어쓴 프로그램이 실행된다.
 exec 함수군은 fork() 함수와 연결해 fork() 함수로 생성한 자식 프로세스가 새로운 프로그램을 실행하도록 할 때도 사용

exec 함수군은 6가지 형태가 있으며 각자 지정하는 인자가 약간씩 다르다

 
• 인자로 전달한 pathname이나 file에 설정한 명령이나 실행 파일을 실행
• arg나 envp로 시작하는 인자를 pathname이나 file에 지정한 파일의 main() 함수에 전달
• 각 함수별로 경로명까지 지정하거나 단순히 실행 파일명만 지정하는 등 차이가 있고 인자를 전달하는 형태에도 차이가 있음
 
1. execl()
• pathname에 지정한 경로명의 파일을 실행하며 arg0~argn 을 인자로 전달
• 첫 인자인 arg0 에는 실행 파일명을 지정
• execl() 함수의 마지막 인자로는 인자의 끝을 의미 하는 NULL 포인터((char*)0)를 지정해야 함
• pathname에 지정하는 경로명은 절대 경로나 상대 경로 모두 사용할 수 있음
 
2. execlp()
• file에 지정한 파일을 실행하며 arg0~argn 만 인자로 전달
• 이 함수를 호출한 프로세스의 검색 경로(환경 변수 PATH에 정의된 경로)에서 파일을 찾음
• arg0~argn 은 포인터로 지정
• execlp() 함수의 마지막 인자는 NULL 포인터로 지정해야 함

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

int main() {
    printf("--> Before exec function\n");
    // execlp() 함수를 호출할 때 ls -a 명령을 실행하도록 지정
    // pathname 자리에 파일 명인 “ls”를 지정했고, 첫 번째 인자에는 실행할 명령인 “ls”를, 마지막 인자로 NULL 포인터를 지정
    if (execlp("ls", "ls", "-a", (char*)NULL) == -1) {
        perror("execlp");
        exit(1);
    }
    printf("--> After exec function\n");

    return 0;
}
// 실헹 결과:  ls –a 명령의 결과가 출력
// execlp() 함수를 만나 프로세스 이미지가 ls 명령의 이미지로 바뀌었으므로
// --> After exec function 출력문은 실행되지 않는다
ls -a 명령의 결과가 출력

 
fork() + execlp() 함수 함께 사용하기

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

int main() {
    pid_t pid;

    switch (pid = fork()) { // fork()로 새로운 프로세스 생성(복제)
        case -1 : /* fork failed */
            perror("fork");
            exit(1);
            break;
        case 0 : /* child process - 자식 프로세스가 수행할 부분: execlp() 함수 호출 */ 
            // 자식 프로세스가 가지고 있던 부모 프로세스 이미지를 ls 명령의 이미지가 덮어쓴다
            // 즉, 부모 프로세스와 자식 프로세스는 각기 다른 프로그램을 실행
            printf("--> Child Process\n");
            if (execlp("ls", "ls", "-a", (char*)NULL) == -1) {
                perror("execlp");
                exit(1);
            }
            exit(0);
            break;
        default : /* parent process */
            printf("--> Parent process - My PID:%d\n", (int)getpid());
            break;
    }

    return 0;
}
// 실행 결과: 자식 프로세스가 "--> Child Process" 문장을 출력한 후 ls –a 명령을 수행했음을 알 수 있음
// 자식 프로세스가 실행되기 전에 부모 프로세스가 먼저 실행되었지만, 다시 실행하면 실행 순서가 달라질 수가 있음

 
3. execle()
• pathname에 지정한 경로명의 파일을 실행하며 arg0~argn 과 envp 를 인자로 전달
• envp에는 새로운 환경 변수를 설정할 수 있음
• arg0~argn 을 포인터로 지정하므로 마지막 값은 NULL 포인터로 지정해야 함
• envp는 포인터 배열
• 이 배열의 마지막에는 NULL 문자열을 저장해야 함
 
 
==함수명에 v가 들어간 함수는 명령행 인자를 포인터 배열로 지정==
4. execv()
• pathname에 지정한 경로명에 있는 파일을 실행하며 argv를 인자로 전달
• argv는 포인터 배열
• 이 배열의 마지막에는 NULL 문자열을 저장해야 함

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

int main() {
    char* argv[3]; // 인자를 저장할 포인터 배열

    printf("Before exec function\n");
    argv[0] = "ls"; // 포인터 배열의
    argv[1] = "-a"; // 각 요소에
    argv[2] = NULL; // 값을 저장
    /*
    argv[0]에는 관례에 따라 실행 파일명을 저장하고 argv[1]에는 옵션을 저장
    더 이상 옵션이나 인자가 없으므로 argv[2]에는 NULL 을 지정
    */
    if (execv("/usr/bin/ls", argv) == -1) { // execv() 함수로 명령의 경로와 포인터 배열 argv를 인자로 지정
        perror("execv");
        exit(1);
    }
    printf("After exec function\n");

    return 0;
}
// 실행 결과: ls -a 명령의 결과를 출력하고 After exec function 문장을 출력하지 않았음을 알 수 있음
ls -a 명령의 결과가 출력

 
+ execve() 함수 호출하기

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

int main() {
    char* argv[3];
    char* envp[2];

    printf("Before exec function\n");
    argv[0] = "arg.out"; // 실행 파일명
    argv[1] = "100"; // 인자
    argv[2] = NULL;

    envp[0] = "MYENV=hanbit"; // 환경 변수 지정
    envp[1] = NULL;
    /*
    argv와 envp 인자를 모두 지정해 execve() 함수를 호출
    이 값들이 제대로 전달되는지 확인하기 위해 arg.out 파일을 별도로 작성
    */
    if (execve("./arg.out", argv, envp) == -1) {
        perror("execve");
        exit(1);
    }
    printf("After exec function\n");

    return 0;
}
지정한 값이 모두 정확하게 전달됨을 알 수 있다

 
arg.out은 아래의 ch7_6_arg.c 파일을 컴파일해 만든다
- arg.out은 인자로 받은 argv와 envp 값을 출력

#include <stdio.h>

int main(int argc, char** argv, char** envp) {
    int n;
    char** env;

    printf("\n--> In ch7_6_arg.c Main\n");
    printf("argc = %d\n", argc);
    for (n = 0; n < argc; n++) {
        printf("argv[%d] = %s\n", n, argv[n]);
    }
    
    env = envp;
    while (*env) {
        printf("%s\n", *env);
        env++;
    }

    return 0;
}

 
5. execvp()
• file에 지정한 파일을 실행하며 argv를 인자로 전달
• argv는 포인터 배열
• 이 배열의 마지막에는 NULL 문자열을 저장해야 함
 
6. execvpe()
• pathname에 지정한 경로명의 파일을 실행하며 argv, envp를 인자로 전달
• argv와 envp는 포인터 배열
• 이 배열의 마지막에는 NULL 문자열을 저장해야 함
 
 
 
ㅁ좀비 프로세스: 종료됐지만, 프로세스 테이블에 여전히 남아있는 프로세스
※ 정상적인 프로세스 종료 과정: 자식 프로세스가 종료를 위해 부모 프로세스에 종료 상태 정보를 전송, 이 정보를 받은 부모 프로세스는 프로세스 테이블에서 자식 프로세스를 삭제
 
ㅇ좀비 프로세스의 발생
- 자식 프로세스가 모든 자원을 반납했어도 부모 프로세스가 종료 상태 정보를 받지 않거나 자식 프로세스보다 먼저
종료하는 경우가 발생
- 실행을 종료한 후 자원을 반납한 자식 프로세스의 종료 상태 정보를 부모 프로세스가 받지 않는 경우에는 좀비 프로세스 발생
 
ㅇ좀비 프로세스의 특징
- 좀비 프로세스는 프로세스 테이블에만 존재
- 일반적인 방법으로 제거할 수 없으며, 부모 프로세스가 wait() 함수를 호출해야 사라짐
- 만일 자식 프로세스보다 부모 프로세스가 먼저 종료되면 자식 프로세스는 고아 프로세스가 됨
- 고아 프로세스는 init(PID 1) 프로세스의 자식 프로세스로 등록
 
 
ㅁ프로세스 동기화
fork() 함수로 자식 프로세스를 생성하면 부모 프로세스와 자식 프로세스는 순서에 관계없이 실행
- 먼저 실행을 마친 프로세스는 종료
- 좀비 프로세스 같은 불안정 상태의 프로세스가 발생
- 따라서 동기화 함수를 사용해 부모 프로세스와 자식 프로세스를 동기화해야 함

 
ㅇ pid_t wait(int* wstatus): 프로세스 동기화
- wstatus : 상태 정보를 저장할 주소
"자식 프로세스가 종료할 때까지 부모 프로세스를 기다리게 한다"
자식 프로세스의 종료 상태는 wstatus에 지정한 주소에 저장 (wstatus에 NULL을 지정할 수도 있음)
 
부모 프로세스가 wait() 함수를 호출하기 전에 자식 프로세스가 종료되면 wait() 함수는 즉시 리턴
wait() 함수의 반환값은 자식 프로세스의 PID (wait() 함수의 반환값이 -1이면 살아있는 자식 프로세스가 하나도 없다는 의미)

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

int main() {
    int status;
    pid_t pid;

    switch (pid = fork()) {
        case -1 : /* fork failed */
            perror("fork");
            exit(1);
            break;
        case 0 : /* child process */
            // 자식 프로세스는 출력을 하나 하고 바로 exit() 함수를 호출해 종료
            printf("--> Child Process\n");
            exit(2); // 종료 상태값으로 2를 지정
            break;
        default : /* parent process */
            // 부모 프로세스가 수행되는 부분에 wait() 함수를 추가
            // 이렇게 하면 자식 프로세스가 "--> Child Process\n" 문장을 출력하고 종료할 때까지 부모 프로세스가 기다림
            // 자식 프로세스가 보낸 종료 상탯값은 status에 저장
            while (wait(&status) != pid) {
                continue;
            }
            // 자식 프로세스가 종료할 때까지 기다리므로 항상 자식 프로세스의 결과가 먼저 출력되고 그다음 아래 문장들이 출력
            printf("--> Parent process\n");
            printf("Status: %d, %x\n", status, status);
            printf("Child process Exit Status:%d\n", status >> 8);
            break;
    }

    return 0;
}
// 실행 결과: 결과가 512(10진수), 0x0200(16진수)으로 출력됨을 알 수 있음
// 자식 프로세스가 전달한 값은 부모 프로세스에 왼쪽으로 한 바이트 이동해 전달
// 이를 제대로 출력하려면 25행처럼 오른쪽으로 8비트 이동시켜야 함
// 25행을 출력한 결과는 종료 상탯값이 2가 됨
자식 프로세스가 "--&amp;gt; Child Process\n" 문장을 출력하고 종료할 때까지 부모 프로세스가 기다림을 알 수 있다

 
ㅇ pid_t waitpid(pid_t pid, int* wstatus, int options): 특정 자식 프로세스와 동기화
- pid : 종료를 기다리는 PID
- status : 종료 상태값을 저장할 주소
- options : waitpid() 함수의 리턴 조건
특정 PID의 자식 프로세스가 종료하기를 기다린다
자식 프로세스의 종료 상태값을 status에 저장하고 options의 조건에 따라 리턴
 
[첫 번째 인자인 pid에 지정할 수 있는 값]
• -1보다 작은 경우 : pid의 절댓값과 같은 프로세스 그룹 ID에 속한 자식 프로세스 중 임의의 프로세스의 상태값을 요청
• -1인 경우 : wait() 함수처럼 임의의 자식 프로세스의 상태값을 요청
• 0인 경우 : 함수를 호출한 프로세스와 같은 프로세스 그룹에 속한 임의의 프로세스의 상태값을 요청
• 0보다 큰 경우 : 지정한 pid에 해당하는 프로세스의 상태값을 요청
 
[세 번째 인자인 options에 지정할 수 있는 값]
sys/wait.h 파일에 정의되어 있으며, OR 연산으로 연결해 지정할 수 있다
- WNOHANG : pid로 지정한 자식 프로세스의 상태값을 즉시 리턴받을 수 없어도 이를 호출한 프로세스의 실행을 블락킹
하지 않고 다른 작업을 수행하게 함

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

int main() {
    int status;
    pid_t pid;

    if ((pid = fork()) < 0) { /* fork failed */
        perror("fork");
        exit(1);
    }
    if (pid == 0) { /* child process */
        printf("--> Child process\n");	
        sleep(3); // 자식 프로세스에서 sleep() 함수를 호출해 곧바로 종료하지 않고 시간을 지연하도록 함
        exit(3);
    }
    printf("--> Parent process\n");
    // 부모 프로세스는 waitpid() 함수를 수행해 자식 프로세스가 종료하기를 기다림
    while (waitpid(pid, &status, WNOHANG) == 0) {
        // WNOHANG 옵션을 지정했으므로 waitpid() 함수는 블로킹되지 않고 반복적으로 실행
        printf("Parent still wait...\n");
        sleep(1);
    }
    printf("Child Exit Status : %d\n", status >> 8);

    return 0;
}
// 실행 결과: "Parent still wait..." 문장이 반복적으로 출력되고 있음을 알 수 있음
// 자식 프로세스의 종료 상태값으로 3이 출력

 
 
 
 
 
 
 
 
참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)