Researcher to Developer

프로세스 생성 시스템 콜 - fork(), exec() - 1 본문

코딩/Basic

프로세스 생성 시스템 콜 - fork(), exec() - 1

Probe29 2020. 12. 31. 17:52

#프로세스의 기본 구조

 

STACK = 함수 처리를 위한 영역

HEAP = 동적 메모리

DATA = 초기값이 있는 변수값 (int i=0;)

BSS = 초기값이 없는 변수값 (int i;)

TEXT = Code, 실행 코드 이미지

 

이 기본 구조에서

STACT, HEAP은 Code가 실행되면서 줄어들거나 커지거나 하지만

DATA, BSS, TEXT는 컴파일시 결정되는 영역이다.

 

아주 간단하게는

전체 메모리 공간을 만들고

프로세스 이미지(코드 이미지)를 해당 공간에 업로드하고 

실행 시작(STACK, HEAP 처리)하므로써 프로세스를 생성한다고 볼 수 있다.

 

 

 

 

#fork()

유닉스, 리눅스 시스템 콜 중 하나

새로운 프로세스 공간을 별도로 만들고, fork() 시스템 콜을 호출한 부모 프로세스 공간의 데이터을 모두 복사한다.

=copy

기존에 실행되고 있었던 부모 프로세스에 있는 모든 내용을 자식 프로세스에 copy한다.

프로세스 안에 있는 fork() 시스템 콜을 실행하면

새로운 프로세스 공간(동일한 코드, 데이터를 가짐)을 만든 다음에

fork() 다음 줄에 Program Counter가 놓여서 동일한 Code를 읽어나가게 된다.

이 때 fork() 의 return value에서 pid가 나오는데

0 일 경우에는 자식 프로세스이고

0 보다 큰 값일 경우 부모 프로세스의 pid로 리턴이 되어서 

자식, 부모 프로세스 간에 실행을 각각 따로 할 수 있다.

 

부모 프로세스가 있는 상태에서
자식 프로세스는 별도의 공간을 만듬
상태 정보가 살아있다.

 

 

 

#fork() 시스템 콜

헤더 파일 <unistd.h>

함수 원형 pid_t fork(void);

특이한 것은 별도의 인자가 없음

pid_t에 대한 return 값은 부모, 자식 프로세스가 다르다.

 

함수는 다음과 같다.

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
             pid_t pid;
             printf("Before fork() call\n");
             pid = fork();

             if (pid == 0)
                 printf("This is Child process. PID is %d\n", pid);
             else if (pid > 0)
                 printf("This is Parent process. PID is %d\n", pid);
             else
                 printf("fork() is failed\n");
             return 0;
}

자식 프로세스는 pid가 0으로 return 되고

부모 프로세스는 pid가 실제 프로세스의 pid으로 return 되어

자식, 부모 프로세스를 pid를 이용해서 구분할 수 있다.

 

그 다음 

gcc forktest.c -o forktest 로 컴파일 해주고

./forktest 라고 입력해서 실행해주면

다음과 같은 결과창을 확인할 수 있다.

Before fork() call
This is Parent process. PID os 1174
사용자@IP주소 This is Child process. PID is 0 

정리하자면
pid = fork()가 실행이 되면 부모 프로세스와 동일한 자식 프로세스가
별도의 메모리 공간에 생성되고
자식 프로세스는 pid가 0으로 리턴

부모 프로세스는 실제 pid값으로 리턴이 된다.
두 프로세스의 상태, 변수 및 PC 값은 동일 pid값만 다른것
새로운 프로세스 공간을 별도로 만들고, fork() 시스템 콜을 호출한 프로세스(부모 프로세스)
공간을 모두 복사한 후, fork() 시스템콜 이후 코드부터 실행한다.

 


 

#exec()

유닉스, 리눅스 시스템 콜 중 하나

=덮어씌움

exec() 시스템 콜을 호출한 현재 프로세스 공간의 TEXT, DATA, BSS 영역을 새로운 프로세스의 이미지로 덮어씌운다.

별도의 프로세스 공간을 만들지 않는다.

 
프로세스 안에 있는 exec() 시스템 콜 실행을 하면 새로운 프로세스를 만드는 것이 아니라
현재 만들어진 exec 인자에 들어가 있는 프로그램 실행 파일을 읽어서 
현재 부모 프로세스 공간의 exec 인자에 있는 실행파일에 대한 
TEXT, DATA, BSS 영역을 덮어 씌운다. (HEAP, STACK 은 동적영역이기 때문)
새로운 정보가 덮어씌워진다.

exec 계열 함수는 현재 프로세스 이미지를 별도 메모리 공간에 복사하지 않는다.

 

 

 

#exec() 시스템 콜 (기본적으로 6가지)
헤더 파일 <unistd.h>
함수 원형   
int execl (const char *path, const char *arg, ...);
int execlp (const char *file, const char *arg, ...);
int execle (const char *path, const char *arg, ..., char *const envp[]);
int execv (const char *path, char *const argv[]);
int execvp (const char *file, char *const argv[]);
int execve (const char *file, const char *argv[], char *const envp[]);

 

 

1. execl()

#include <unistd.h>            //execl 때문에 사용한 라이브러리
#include <stdio.h>              //printf 때문에 사용한 라이브러리
#include <stdlib.h>             //perror 떄문에 사용한 라이브러리
int main()
{
                 printf(execute is\n");
                 execl("/bin/ls", "ls", "-al", NULL); 
                 perror("execl is failed\n");       //에러 코드 출력
                 exit(1);                                //에러 코드 전달
}

* bin/ls 전체 실행파일 이름을 인자로 적어주고 2,3번째 인자는 각각 argv[0], argv[1] 에 해당하는 내용이다.

끝에는 무조건 NULL 을 쓰게되어있다.

 

하나의 프로세스 공간에 저장되어 있는 code에서
execl 코드를 만나면 프로세스의 Code영역이 덮어씌워지고
PC가 ls(일종의 실행파일) 첫 줄 부터 실행하게 된다.
그리고 perror, exit 가 실행이 되면 execl 이 코드가 실패했다는 의미이다.
즉, perror () 함수가 호출된다는 의미는 새로운 프로세스 이미지로 덮어씌우는 작업이 실행되지 못했다는 의미
그래서 원래 대로 하면 perror, exit 는 정상적인 코드에서 실행이 되면 안된다.

컴파일 해준다음
실행을 해주면
실제 ls -al 명령어와 같은 결과창을 확인할 수 있다.

 

 

 

#명령어 인수 리스트

argc 는 인자갯수를 나타내고  
argv[] 는 인자를 0번 부터 해서 값을 넣게 해줌 
argv[0] 은 무조건 실행파일 이름임

 

ls -al
에서 ls 는 argv[0] 이고 -al 은 argv[1] 임

 

 

 

#execl(), execlp(), execle() 시스템콜 사용법

1. execl

execl("/bin/ls", "ls", "-al", NULL);  

("디렉토리와 파일 이름이 합친 전체 이름", "명령어 인수 리스트", "끝은 NULL로 끝나야 함");

파일 이름으로 해당 프로세스로 실행한 프로세스의 환경변수 중 하나인 path를 검색함
파일 이름만 넘겨주면 프로세스 환경변수(path)에서 파일명(실행 파일) 검색

 

 

 

 

2. execlp

execl("ls", "ls", "-al", NULL);  

("파일 이름", "명령어 인수 리스트", "끝은 NULL로 끝나야 함");


default 로 설정되어 있는 디렉토리 명을 참조해서 실행하겠다는 의미!

 

 

 

 

* 다음은 인자를 일종의 변수로 미리 만들어서 넣을 때 사용하는 exec() 시스템 콜이다.
3. execle()

char *envp[] = {"USER=dave", "PATH=/bin", (char *)0}; 
execle("is", "ls", "-al", NULL, envp); 

환경 변수를 별도로 설정하고자 할 때 envp에 작성, 미리 환경변수를 정의해서 넘겨줘야한다.
디렉토리 명을 안쓰고 환경 변수로 지정하는 거지

execl 와 인자는 같지만 NULL 뒤에 envp(string 변수)를 지정해주어야 한다.

 



4. execv()

char *arg[] = {"is", "-al", NULL};                // 인수 리스트를 내용으로 하는 문자열 배열
execv("/bin/ls", arg); 

 

 

 

 

 

5. execvp()

char *arg[] = {"is", "-al", NULL};               //인수 리스트를 내용으로 하는 문자열 배열
execvp("is", arg);

파일 이름을 해당 프로세스를 실행한 프로세스의 환경변수 중 하나인 path를 검색함 

 

 

 

 

 

6. execve()

char *envp[] = {"USER=dave", "PATH=/bin", (char *)0};      // 환경 변수를 지정하고자 할 때 
char *arg[] = {"is", "-al", NULL};                                    // 인수 리스트를 내용으로 하는 문자열 배열 
execve("is", arg, envp);  


* execve 예제

#include <unistd.h>   
#include <stdio.h>    
#include <stdlib.h>   
int main() 
{         
          char *envp[] = {"USER=dave", NULL};        #환경 변수 
          char *arg[] = {"ls", "-al", NULL};                #인자 

          printf("execute ls\n")' 
          execve("ls", arg, envp); 
          perror("execl is failed\n"); 
          exit(1); 


여기서 환경 변수에 파일 이름(path)을 넣어주지 않았기 때문에 execl is failed 라는 내용이 나올 것

 

 

 

 

 

 

execlp, execvp 는 파일명만 넘겨주면 프로세스 환경변수(path)에서 파일명(실행파일)을 검색하겠다

execl, execv 는 파일명을 전체 경로로 포함해서 넘겨줘야하고

execle. execve 는 환경변수(path 포함 가능)를 별도로 정의해서 넘겨줘야한다.

 v 와 l 의 차이 : argv(인자)를 설정해서 넣겠냐 아니면 직접 써주겠냐의 차이
e : 환경변수를 넘겨줄 때 사용
p : p가 있는 경우 환경변수 path를 참조하기 때문에 경로를 입력하지 않아도 된다.