Love Every Moment

〔LINUX/UNIX〕프로세스 생성과 실행: pid, fork, exec, pipe 본문

PROGRAMMING::CORE/Operating System

〔LINUX/UNIX〕프로세스 생성과 실행: pid, fork, exec, pipe

해 송 2021. 6. 25. 22:20
반응형

 

1. 프로세스(Process)

 

 

(1) 프로세스란?

  • 현재 실행 중인 프로그램
  • 즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당 받아 실행중인 것
  • 운영체제의 제어 아래 실행(Running), 대기(Waiting), 중단(Stopped), 좀비(Zombie) 중 하나의 상태에 있게 된다.
  • 각각의 프로세스가 가지는 고유의 번호를 PID(Process Idendification Number)라고 한다.
  • 예를 들어 데비안 리눅스가 부팅될 때에는 최상위 프로세스인 systemd(PID: 1)이 생성되며 모든 프로세스들은 이 1번 프로세스의 자식 프로세스가 된다.
  • 부모 프로세스의 PID 를 줄여서 PPID 라고 한다.
  • ps 명령어를 통해 현재 실행중인 프로세스 목록을 확인할 수 있다.

 

 

(2) 쓰레드(Thread)

  • 프로세스 내에서 실제로 작업을 수행하는 주체
  • 모든 프로세스에는 하나 이상의 쓰레드가 존재한다.
  • 두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스라고 한다.
  • 특정한 시점에 프로그램의 특정한 부분을 수행하는 각각이 모두 쓰레드이다.
  • 따라서 여러 개의 쓰레드를 이용하여 프로그램의 여러 부분을 동시에 수행시킬 수 있다.

 

싱글 쓰레드 프로세스와 멀티 쓰레드 프로세스 비교

 

각각의 프로세스들은 각각의 독립된 영역을 가지는 반면, 쓰레드는 하나의 프로세스 내에서 여러개 존재하기 때문에 같은 프로세스에 있는 메모리 공간을 서로 공유한다. 위의 사진에서 세 개의 쓰레드는 하나의 주소 공간에 존재하며 같은 데이터와 파일을 서로 공유하면서 코드의 여러 부분을 동시에 수행한다.

이 멀티 쓰레드가 각각 독립적으로 수행되기 위해서 가지는 두 가지 정보가 바로 CPU의 프로그램 카운터와 스택이다.

프로그램 카운터(Program Counter): 프로그램의 어느 부분을 실행하고 있는지에 대한 정보를 저장
스택(Stack): 함수를 호출하는 순서(Function Call)에 대한 정보를 저장 

 

 

 

[Linux] 프로세스(Process) 및 쓰레드(Thread) 개념

프로세스 및 쓰레드 개념 프로세스란? 프로세스란 단순히 실행 중인 프로그램이라고 할 수 있다. 즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당 받아 실행중인 것을 말한

eehoeskrap.tistory.com

 

 


 

2. 프로세스 생성과 실행

 

사용자가 명령행에서 직접 프로세스를 실행하여 생성하는 경우 이외에도 프로그램 내에서 다른 프로그램을 실행하여 생성하는 경우도 있다. 위와 같이 system(), fork(), 또는 vfork() 함수를 통해 프로세스를 실행하거나 생성할 수 있다.

프로그램 실행 int system(const char *string);
프로세스 생성 pid_t fork(void);  또는 pid_t vfork(void);

 

(1) system()

  • system() 함수를 이용하여 간단하게 프로그램 안에서 새로운 프로그램을 실행시킬 수 있다.
  • 기존의 명령이나 실행 파일명을 인자로 받아 쉘에 전달한다.
  • 쉘은 내부적으로 새로운 프로세스를 생성하여 인자로 받은 명령을 실행한다.
  • 해당 명령이 끝날 때까지 기다렸다가 종료 상태를 리턴한다.
  • 가장 간단하지만 명령을 실행하기 위해 쉘까지 동작시키기 때문에 비효율적인 방법이다.
 system("leaks a.out");
→ main 문 안에 적으면 프로그램이 종료되기 전에 메모리 누수를 확인해준다.

 

 


 

 

(2) fork()

  • fork() 함수로 생성된 새로운 프로세스를 자식 프로세스(Child Process)라고 한다.
  • fork() 함수를 호출한 프로세스는 부모 프로세스(Parent Process)가 된다.
  • fork() 함수가 리턴하면 부모 프로세스와 자식 프로세스가 동시에 동작하게 된다.
  • 어느 프로세스가 먼저 실행되는지는 알 수 없으며 유닉스 운영체제의 스케쥴링에 따라 처리 순서가 달라진다.

 

fork() 함수 처리 과정:

#1. fork() 함수 호출
#2. 새로운 프로세스(=자식 프로세스) 생성
#3. 부모 프로세스의 데이터 영역을 그대로 복사하여 자식 프로세스에 메모리를 할당해 준다.
#4. 부모 프로세스에는 자식 프로세스의 PID를, 자식 프로세스에는 0 을 반환한다.

 

 


 

 

(3) exec()

현재 프로그램의 텍스트, 데이터, 스택 영역에 exec() 계열의 함수의 인자로 전달된 프로그램의 텍스트, 데이터, 스택 영역을 덮어씌우는 함수이다. 즉, 새로운 프로세스가 현재 프로세스 위치에 덮어씌워지므로 현재 프로세스는 종료된다.

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

int main()
{
        printf("exec()함수 호출전\n");
        execl("/bin/ls" , "ls" , "-l" , (char *) 0);
        printf("exec()함수 호출 후\n");

        return 0;
}

 

위의 예시에서 출력되는 것은 "exec() 함수 호출전" 뿐이다. execl()이 ls 프로세스를 실행함과 동시에 현재 프로세스가 종료되었기 때문이다. 만약 현재 프로세스를 종료시키고 싶지 않다면 아래처럼 fork() 함수를 이용한 뒤에 exec() 을 사용하면 된다.

 

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

int main()
{
        int pid;

        if((pid=fork()) == 0)
        {
                printf("Child Process ID:%d\n", getpid());
                execl("/bin/ls", "ls", "-l", (char *)0);
        }
        else
        {
                printf("Parent Process ID: %d\n", getpid());
        }
        return 0;
}

 

자식 프로세스에서 exec 함수를 호출하면 부모 프로세스로부터 복사한 프로그램과는 다른 명령이나 프로그램을 실행 가능하다. 부모 프로세스와 자식 프로세스가 각각 다른 작업을 수행해야 하는 경우 fork()와 exec() 함수를 함께 사용해야한다.

예를 들어 쉘에서 'ls -a' 라는 명령어를 입력 받으면 쉘은 fork() 함수로 자식 프로세스를 만들고 사용자가 입력한 명령 'ls -a'를 exec() 함수군을 사용하여 자식 프로세스에서 실행한다.

 

 

■ execve()

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);

 

  • filename: 실행 가능한 binary file, shell script, 또는 명령어
  • argv: main 에서의 argv 와 똑같지만, main 과 달리 argc 가 없으므로 마지막에는 NULL 이 있다.
  • envp: key=value 형식으로 구성되어 있는 환경 변수 문자열 배열리스트로 마지막에는 NULL 이 있다. 예를 들어 envp[0] 이 "HOME=/home/user" 라면 key 는 "HOME", value 는 "/home/user" 가 된다. 만약 이미 설정된 환경 변수를 사용하려면 environ 환경 변수를 사용할 수 있다. environ 은 C 프로그램에서 이미 선언되어 있기 때문에 extern 문을 통해 environ 변수를 참조하여 환경 변수 목록을 확인할 수 있다.

 

#include <stdio.h>

extern char **environ;

int main()
{
    int i = 0;

    printf("=== environment list ===\n");

    for(i=0; environ[i];i++)
    {
        printf("<%2d>: %s\n", i, environ[i]);
    }
    return 0;
}

 

ls 명령어 실행하는 프로그램 만들기

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

extern char **environ;

int	main(int argc, char *argv[])
{
	char	**new_argv;
	char	command[] = "ls";
	int		idx;

	new_argv = (char **)malloc(sizeof(char *) * (argc + 1));

	new_argv[0] = command; // 명령어를 ls로 변경

	// command line으로 넘어온 parameter를 그대로 사용
	for (idx = 1; idx < argc; idx++)
		new_argv[idx] = argv[idx];

	// argc를 execve 파라미터에 전달할 수 없기 때문에 NULL이 파라미터의 끝을 의미한다.
	new_argv[argc] = NULL;
	if (execve("/bin/ls", new_argv, environ) == -1)
	{
		fprintf(stderr, "프로그램 실행 error: %s\n", strerror(errno));
		return 1;
	}

	// ls 명령어 binary로 실행로직이 교체되었으므로 이후의 로직은 절대 실행되지 않는다.
	printf("이것은 이제 ls 명령어러 이 라인은 출력되지 않는다. \n");
	return 0;
}

 

 

 

[Unix] execve

실행 가능한 파일인 fileaname의 실행 코드를 현재 프로세스에 적재하여 기존의 실행코드와 교체하여 새로운 기능으로 실행한다. 즉, 현재 실행되는 프로그램의 기능은 없어지고 filename 프로그램

velog.io

 

 


 

 

(4)  wait()

fork() 함수를 통해 자식 프로세스를 생성하고 나면 부모 프로세스와 자식 프로세스 중에 어느 것이 먼저 실행되는지 알 수 없으며 먼저 실행을 마친 프로세스는 종료한다. 하지만 이들 사이의 종료 절차가 제대로 진행되지 않는 경우가 있다. 이 때 좀비 프로세스(Zombie Process)라는 불안정 상태의 프로세스가 발생한다. 이를 방지하기 위해 필요한 것이 프로세스 동기화이다.

부모 프로세스와 자식 프로세스를 동기화하려면 부모 프로세스는 자식 프로세스가 종료할 때까지 기다려야한다. 여기서 자식 프로세스의 실행이 완전히 끝나기를 기다렸다가 종료 상태를 확인하는 함수가 wait() 함수이다.

자식 프로세스의 종료 상태는 stat_loc 에 지정한 주소에 저장된다. 만약 부모 프로세스가 wait() 함수를 호출하기 전에 자식 프로세스가 종료하면 wait() 함수는 즉시 리턴한다. 리턴값은 자식 프로세스의 ID 이다. 만약 리턴값이 -1 이라면 살아있는 자식 프로세스가 하나도 없다는 것을 의미한다.

※ wait() 함수의 경우 아무 자식 프로세스나 종료하면 리턴하지만 waitpid() 함수는 특정 PID 의 자식 프로세스가 종료하기를 기다린다는 점에서 다르다.

 

#include <sys/type.h>
#include <sys/wait.h>

pid_t wait(int *stat_loc);
※ stat_loc: 상태 정보를 저장하는 주소

pid_t waitpid(pid_t pid, int *status, int options);
※ status: 프로세스의 상태정보를 저장하는 주소

 

waitpid() 함수에서 status 는 프로세스의 상태를 나타내기 위해 사용된다. status 가 NULL 이 아닌 경우, status 가 가리키는 위치에 프로세스의 상태정보를 저장한다. 여기서 상태정보를 알아내기 위해 사용되는 매크로들 중에서 가장 대표적인 것이 WIFEXITED(status) 이다. 자식 프로세스가 정상적으로 종료되었다면 TRUE(non-zero) 이다.

 

 

6. 프로세스 생성과 실행 · UNIXBasic

 

jihooyim1.gitbooks.io

 

linux man page : waitpid - 자식 프로세스의 종료를 기다린다.

프로세스의 종료를 기다린다. waitpid 함수는 인수로 주어진 pid 번호의 자식프로세스가 종료되거나, 시그널 함수를 호출하는 신호가 전달될때까지 waitpid 호출한 영역에서 일시 중지 된다. 만일 pid

www.joinc.co.kr

 


 

3. pipe()

(1) 형식

#include <unistd.h>
int pipe(int fd[2]);

 

(2) 작동 원리

출처: https://reakwon.tistory.com/80

 

  • pipe 함수가 성공적으로 호출되는 경우 0, 실패했을 경우에는 -1 을 반환
  • 파이프는 사진과 같이 커널 영역에 생성되며 프로세스는 파일 디스크립터만을 가지고 있다.
  • 파일 디스크립터 fd[0] 은 읽기용 파이프이고 fd[1] 은 쓰기용 파이프이다.
  • 현재 프로세스에서 fork() 하게되면 자식 프로세스가 생성된다.
  • 부모 프로세스의 파일 디스크립터는 그대로 자식 프로세스에 복제된다. 

 

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1

int main(){
        int fd[2];
        pid_t pid;
        char buf[MAX_BUF];

        if(pipe(fd) < 0)
        {
                printf("pipe error\n");
                exit(1);
        }
        if((pid=fork())<0)
        {
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0) //parent process
        {
                close(fd[READ]);
                strcpy(buf,"message from parent\n");
                write(fd[WRITE],buf,strlen(buf));
        }
        else     //child process
        {
                close(fd[WRITE]);
                read(fd[READ],buf,MAX_BUF);
                printf("child got message : %s\n",buf);
        }
        exit(0);
}
  • 우선 부모 프로세스에서 파이프를 생성하여 파이프에 데이터를 쓰기(fd[1]: stdout)만 할것이므로 읽기 파이프(fd[0]: stdin)는 닫고, fd[1] 에 데이터를 쓴다.
  • 자식 프로세스에서 쓰기 파이프는 쓰지 않으므로 fd[1] 은 닫고 읽기 파이프를 통해 데이터를 읽는다.
  • 출력 결과는 child got message : message from parent 이 된다.

 

 

[리눅스] 파이프(pipe) 개념과 예제

파이프(Pipe) 파이프(Pipe)란 프로세스간 통신을 할때 사용하는 커뮤니케이션의 한 방법입니다. 가장 오래된 UNIX 시스템의 IPC로 모든 유닉스 시스템이 제공합니다. 하지만 두가지 정도의 한계점이

reakwon.tistory.com

 

 

반응형
Comments