4-6 Process (학습)

do·2022년 5월 2일
0

API

목록 보기
29/42

1. 리눅스 프로세스 기본 개념

  • 프로세스
    • 운영체제에 의해 메모리 공간을 할당받아 CPU에서 실행/제어되고 있는 프로그램
    • 프로세스의 주소 공간은 code, data, stack, heap 영역으로 구성된다.
    • 프로세스를 생성하여 자원을 할당하는 시스템 콜이 줄어들어 자원을 효율적으로 관리할 수 있다.
    • 프로세스에는 2가지의 종류의 user가 있다. (real user, effective user)
  • Process ID
    • 구별하기 위해 각 프로세스들에게 부여한 고유번호
  • real UID
    • 프로세스를 실행한 사용자의 ID
    • realUID는 부모 프로세스의 realUID로 설정된다.
    • exec() 호출 도중에 변경되지 않는다.
    • 루트 권한을 갖는 프로세스만 realUID를 다른 값으로 변경할 수 있다.
    • 루트 권한을 제외하고는, 생성하는 자식 프로세스들은 같은 realUID를 가진다.
  • effective UID
    • 프로세스가 실행되는 동안에만 부여되는 UID며, 접근 권한을 판단할 때 사용한다.
    • 프로세스를 처음 생성할 때는 부모 프로세스의 effectiveUID를 상속받는다. 따라서 EUID와 RUID가 같다.
    • 프로세스는 EUID를 변경할 수 있다. (setUID bit가 설정되어 있는 프로그램을 실행할 때만)
  • real GID
    • 프로세스를 실행한 사용자의 기본 그룹 ID
  • effective GID
    • 프로세스가 실행되는 동안에만 부여한 GID며, 접근 권한을 판단할 때 사용한다.
  • 부모 프로세스
    • 다른 프로세스를 생성하는 프로세스
  • 자식 프로세스
    • 다른 프로세스에 의해 생성된 프로세스
  • 좀비 프로세스
    • 자식 프로세스가 부모 프로세스보다 먼저 종료되는 경우
    • 자식이 exit()를 호출하면서 종료되면 이 프로세스에 관련된 모든 메모리와 자원이 해제되어 다른 프로세스에서 사용할 수 있다.
    • 부모가 자식의 종료 상태를 회수하기 위해, 커널은 자식의 최소한의 정보(PID, 종료 상태 등)를 가진다.
    • 부모가 좀비의 종료 상태를 회수하게 되면 wait()을 통하여 좀비는 제거된다.
    • (커널 입장에서 좀비 프로세스는 최소한의 정보만을 가지고 있어 큰 성능 저하를 야기하지는 않지만, 프로세스 스케쥴링에 있어서 queue에 대기하고 있는 프로세스의 양이 증가하게 되고, 커널 구조체를 유지하기 위한 비용 또한 무시할 수 없다.)
  • 고아 프로세스
    • 부모가 자식보다 먼저 종료되는 경우
    • init(초기 프로세스)가 새로운 부모 프로세스가 된다.
    • 종료되는 프로세스가 발생할 때 커널은 이 프로세스가 누구의 부모인지 확인 후, 커널이 자식의 부모 ID를 1(init 프로세스)로 바꿔준다.
    • 고아 프로세스가 작업을 종료하면 init 프로세스가 wait() 함수를 호출하여 고아 프로세스의 종료 상태를 회수함으로써 좀비가 되는 것을 방지한다.
    • (고아는 init이 관리하지만 부모가 종료되기 전에 모든 자식을 wait()하는 것이 좋다.)
  • 멀티 프로세스의 특징
    • 안정성이 좋다. 여러 개의 자식 프로세스 중 하나에 문제가 발생해도, 다른 자식 프로세스에 영향이 확산되지 않는다.
    • 구현이 비교적 간단하고, 각 프로세스들이 독립적으로 동작하며 자원에 서로 다르게 할당된다.
    • 프로세스 간 통신을 하기 위해서는 IPC를 통해야한다.
    • 메모리 사용량이 많다.
    • 스케쥴링에 따른 Context Switch가 많아지고, 성능 저하의 우려가 있다.
      • Context Switch(문맥 교환): 하나의 프로세스가 CPU를 사용 중인 상태에서, 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태를 보관하고 새로운 프로세스의 상태를 적재하는 작업
  • fork()와 exec()의 차이
    • 모두 한 프로세스가 다른 프로세스를 실행시키기 위해 사용한다.
    • fork() 새로운 프로세스를 위한 메모리를 할당한다. (프로세스가 하나 더 생기는 것)
    • 반면, exec()는 fork()처럼 새로운 프로세스를 위한 메모리를 할당하지 않고, exec()를 호출한 프로세스가 아닌, exec()에 의해 호출된 프로세스만 메모리에 남는다.
    • (exec()를 호출한 프로세스의 PID가 그대로 새로운 프로세스에 적용되며, exec()를 호출한 프로세스는 새로운 프로세스에 의해 덮어쓰여진다.)
  • exec()와 system()의 차이
    • exec()는 호출 프로세스가 바뀐다. 반면에 system()은 작업이 끝날 때까지 호출한 프로세스가 대기하다가 계속 수행한다.
    • exec()는 어떠한 값도 리턴하지 않으며 단순히 명령어를 수행한다. 반면에 system()은 fork를 하고 명령어 실행이 성공했는지 실패했는지 기다리며 값을 리턴해줍니다.
    • exec()를 호출했을 때는, 현재 프로그램이 (예시)/bin/ls 라는 프로그램을 덮어 씌워져 printf() 함수가 출력되지 않는다. 반면에 system()을 호출했을 때는, 명령어 실행 후 원래 프로그램으로 돌아와서 남아있는 printf()를 호출한다.
	char buf[255];
	sprintf(buf, "grep -n %s %s", search, path);
	system(buf);
	printf("system함수가 실행되었습니다.\n"); //실행됨
    
    execl("/bin/grep", "grep", "-n", search, path, NULL);    
	printf("exec함수가 실행되었습니다.\n"); //실행되지 않음

2. 프로세스 시스템콜

1. fork()

함수 원형. pid_t fork(void)
기능. 새 프로세스를 생성한다.
헤더. <unistd.h>
리턴값1. 부모 프로세스에서는 자식 프로세스의 PID를 반환받는다.
리턴값2. 자식 프로세스에서는 0을 반환받는다.
리턴값3. -1 errno

  • fork() 함수를 호출하는 프로세스는 부모 프로세스가 되고, 생성된 프로세스가 자식이 된다.
  • 생성된 자식 프로세스는 부모 프로세스의 내용을 그대로 복사하여 가진다.
  • fork 함수 호출 이후 코드부터 각자의 메모리를 사용하여 실행된다.

2. wait()

함수 원형. pid_t wait(int *status)
기능. 부모가 자식이 종료했음을 확인한다.
하위 프로세스가 끝날 때까지 현재 프로세스의 실행을 잠시 정지한다. 종료 상태 값을 의식하지 않으면 매개변수 status를 NULL로 설정할 수 있다.
헤더. <wait.h>
매개변수. int *status
리턴값1. pid_t PID
리턴값2. -1 errno

  • 부모가 자식의 종료 상태를 얻기 위해 사용된다.
  • 자식 프로세스가 동작 중이면 호출이 차단되기 때문에, 상태를 얻어올 때까지 대기한다.
  • wait() 호출자가 시그널을 받을 때까지 대기한다.
  • 자식 프로세스가 종료된 상태라면 즉시 호출이 반환되어 상태를 얻는다. (자식 프로세스의 PID 반환)
  • 자식 프로세스가 없다면 호출이 즉시 반환되며, 에러값을 반환한다.

3. waitpid()

함수 원형. pid_t waitpid(pid_t pid, int *status, int options)
기능. 특정 자식 프로세스의 종료를 기다릴 수 있다.
헤더. <wait.h>
매개변수1. pid_t pid
매개변수2. int *status 자식 프로세스 상태 확인가능
매개변수3. int options
리턴값1. pid_t PID
리턴값2. -1 errno

  • 매개변수1 pid_t pid
pid(자식 PID)의 값디테일
pid < -1pid의 절대값과 동일한 프로세스 그룹ID의 모든 자식 프로세스의 종료를 기다림
pid == -1임의의 자식 프로세스의 종료를 기다림
pid == 0현재 프로세스의 프로세스 그룹ID와 같은 프로세스 그룹ID를 가지는 모든 자식 프로세스의 종료를 기다림
pid > 0pid값에 해당하는 프로세스의 종료를 기다림
  • 매개변수2 int *status
매크로디테일
WIFEXITED(status)0이 아닌 값을 리턴하면 자식 프로세스가 정상 종료했다는 뜻
WEXITSTATUS(status)이 매크로를 통하여 자식 프로세스가 정상 종료했음을 확인하면, 종료 코드를 확인 할 수 있음. 이 종료 코드는 exit(),_exit()에서 인자로 주는 값을 말함. 즉 exit(0)으로 프로그램을 종료했다면, 0 값이 WIFEXITED 매크로로 알 수 있음. 단, 이 매크로는 하위 8비트 값만을 확인하므로 0부터 255까지의 값까지 확인할 수 있음.
WIFSIGNALED(status)이 매크로가 참이라면 자식 프로세스사 비정상 종료했다는 뜻
WTERMSIG(status)WIFSIGNALED(stauts) 매크로가 참일 경우, 자식 프로세스를 종료시킨 시그널 번호를 얻는 매크로
WIFSTOPPED(status)이 매크로가 참이면, 자식 프로세스는 현재 멈춰있는 상태임. options 인자에 WUNTRACED 옵션이 설정되어 있는 경우 자식 프로세스의 멈춤 상태를 알아낼 수 있음.
WSTOPSIG(status)WIFSTOPPED(status)가 참이면, 자식 프로세스를 멈춤 상태로 만든 시그널 번호를 얻음
WCOREDUMP(status)시스템에 따라서는 WIFSIGNALED(status)가 참일 경우, 자식 프로세스가 core덤프 파일을 생성했는지를 확인하는 매크로를 제공해주기도 함
  • 매개변수3 int options
option의미
WNOHANGwaitpid()를 실행했을때, 자식 프로세스가 종료되어 있지 않으면 블록상태가 되지 않고 바로 리턴하게 해줌
WUNTRACEpid에 해당하는 자식 프로세스가 멈춤 상태일 경우, 그 상태를 리턴함. 즉, 프로세스의 종료 뿐만 아니라 멈춤 상태도 찾아냄

4. execl()

함수 원형. int execl(const char *path, const char *arg0, .., const char *argn, /* (char *)NULL */)
기능. exec + l (인자의 목록 list)
fork를 통해 새로운 프로세스를 생성했다면, 실행되는 중에 다른 형태의 프로세스로 변하고 싶을 때 사용한다.
현재 실행중인 메모리를 비우고 처음부터 다시 시작하게 해준다.
path 경로명의 파일을 실행하며, arg0~argn을 인자로 전달한다.
관례적으로 arg0에는 실행 파일명을 지정하고, 마지막 인자로는 끝을 의미하는 NULL을 지정해야한다.
헤더. <unistd.h>
매개변수1. const char *path
매개변수2~n. const char *arg0~*argn
매개변수z. NULL
리턴값. int 에러가 있을 때만 -1을 리턴한다.

5. execle()

함수 원형. int execle(const char *path, const char *arg0, .., const char *argn, /* (char *)NULL, char *const envp[] */)
기능. exec + l (인자의 목록 list) + e(환경변수 전달가능 environment)
실행가능한 파일인 path의 실행코드를 현재 프로세스에 적재하여 기존의 실행코드와 교체하여 새로운 기능으로 실행한다. 즉, 현재 실행되는 프로그램의 기능은 없어지고 path 프로그램을 메모리에 loading하여 처음부터 실행한다.
헤더. <unistd.h>
매개변수1. const char *path 명령어가 위치한 절대 path 또는 상대 path를 포함한 명령어 (ex. /usr/bin/ls)
매개변수2~n. const char *arg0~*argn 명령어 다음에 오는 파라미터를 순서대로 나열하며, 끝은 NULL로 끝나야 한다. (ex. ls)
매개변수z. char *const envp[] path 명령어로 교체되면서 적용될 환경변수 (만약 현재의 환경변수를 그대로 유지하려면, environ 전역변수를 parameter로 전달하면 된다.)
리턴값1. 없음 정상적으로 처리되면 execle() 다음 로직은 실행할 수 없다. 이미 다른 프로그램이 되었기 때문이다.
리턴값2. -1 binary 교체가 실패하였으며, 상세한 오류 내용은 errno에 설정된다.

execle.c

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

//현재의 환경변수를 그대로 유지하려면, environ 전역변수를 parameter로 전달하면 된다.
extern char **environ;

int main(int argc, char *argv[])
{
        if (execle("/bin/bash", "bash", NULL, environ) == -1) {
                perror("execle()");
        }

        /*ls 명령어 binary로 실행로직이 교체되었으므로 이후의 로직은 실행되지 않는다. */
        printf("this is ls.\n");

        return 0;
}

execle2_1.c

/* 환경변수 출력하는 프로그램 */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
        if (argc > 1) {
                printf("환경변수 %s는 %s 입니다.\n", argv[1], getenv(argv[1]));
        }
        else {
                printf("실행방법: show_envp[환경변수]\n");
        }

        return 0;
}

execle2_2.c

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

int main()
{
        char *envp[] = { "TEST=ttttttt", NULL };

        execle("./execle2_1", "./execle2_1", "TEST", NULL, envp);

        printf("이 메세지가 보이면 안됨\n");

        return 0;
}

6. execv()

함수 원형. int execv(const char *path, char *const argv[])
기능. exec + v(인자 배열)
헤더. <unistd.h>
매개변수1. const char *path 실행 파일의 디렉토리 포함 전체 파일 명
매개변수2. char *const argv[] 인수 목록
리턴값. int 실패할 때만 -1

함수 이름프로그램 지정명령라인 인수함수 설명
execl디렉토리와 파일 이름이 합친 전체 이름인수 리스트환경 설정 불가
execle디렉토리와 파일 이름이 합친 전체 이름인수 리스트환경 설정 가능
execv디렉토리와 파일 이름이 합친 전체 이름인수 배열환경 설정 불가

7. system()

함수 원형. int system(const char* command)
기능. 시스템 명령을 수행한다.
헤더. <stdlib.h>
매개변수. const char* command 명령어
리턴값. int 명령을 수행하여 해당 프로세스가 종료하면, 종료할 때의 값을 반환한다.

196-245

0개의 댓글