본문 바로가기
Computer Science

시스템 프로그래밍 - 프로세스 관리

by 개발자 데이빗 2022. 1. 10.

프로세스 관리

  • 프로그램: 바이너리, 코드 이미지, 응용프로그램 또는 실행 파일
  • 프로세스: 실행 중인 프로그램 (메모리 적재 + 프로세스 상태 정보 포함)
  • 스레드
    • 리눅스 프로세스는 기본 스레드 포함
    • 싱글 스레드 프로세스: 기본 프로세스
    • 멀티 스레드 프로세싀: 여러 스레드 존재

프로세스 ID

  • pid, 각 프로세스는 해당 시점에 unique한 pid를 가짐
  • pid 최대 값은 32768 = 2의 15승
  • 부호형 16비트 정수값 사용
  • 최근 할당된 pid가 200이라면, 그 이후는 201, 202 식으로 할당
$ sudo vi/proc/sys/kernel/pid_max

 

 

프로세스 계층

  • 최초 프로세스: init 프로세스, pid 1
  • init 프로세스는 운영체제가 생성 (단군 할아버지)
  • 다른 프로세스는 또다른 프로세스로부터 생성 (부모 프로세스, 자식 프로세스)
  • ppid값이 부모 프로세스의 pid를 뜻함

ps -ef

  • -e: 시스템상의 모든 프로세스에 대한 정보 출력
  • -f: 다음 목록 출력(UID, PID, PPID, CPU%, STIME, TTY, TIME, CMD)

프로세스와 소유자 관리

리눅스 내부에서는 프로세스의 소유자와 그룹을 UID/GID (정수)로 관리

사용자에 보여줄때에만 UID와 사용자이름 매핑 정보를 기반으로 사용자 이름으로 제공

$ ps -ef
$ sudo vi /etc/passwd
$ sudo vi /etc/shadow

 

프로세스 관리 관련 시스템콜

해당 프로세스의 pid와 부모 프로세스의 id(ppid)

  • 함수 원형
#include <sys/types.h>
#include <unistd.h>
pid_t getpid (void);
pid_t getppid (void);
  • 실습 코드
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
	printf ("pid=%d\n", getpid());
	printf ("ppid=%d\n", getppid());
	return 0;
}

 

 

프로세스 생성

  • 기본 프로세스 생성 과정
    • TEXT, DATA, BSS, HEAP, STACK의 공간을 생성
    • 프로세스 이미지를 해당 공간에 업로드하고, 실행 시작
  • 프로세스 계층: 다른 프로세스는 또다른 프로세스로부터 생성
    • 부모 프로세스, 자식 프로세스

fork()와 exec() 시스템콜

fork() 와 exec() 이 순차적으로 실행 되어 새로운 프로세스를 만든다.

  • fork() 
    • 새로운 프로세스 공간을 별도로 만들고 fork() 시스템콜을 호출한 프로세스(부모 프로세스) 공간을 모두 복사
    • 별도의 프로세스 공간을 만들고, 부모 프로세스 공간의 데이터를 그대로 복사
  • exec()
    • exec() 시스템콜을 호출한 현재 프로세스 공간의 TEXT, DATA, BSS 영역을 새로운 프로세스의 이미지로 덮어씌움
    • 별도의 프로세스 공간을 만들지 않음

fork() 시스템콜

  • pid = fork()가 실행되면 부모 프로세스와 동일한 자식 프로세스가 별도 메모리 공간에 생성
  • 자식 프로세스는 pid가 0으로 리턴, 부모 프로세스는 실제 pid 반환
  • 두 프로세스의 변수 및 PC 값은 동일
  • 새로운 프로세스 공간을 별도로 만들고, fork() 시스템콜을 호출한 프로세스 공간을 모두 복사한 후 fork() 시스템콜 이후 코드부터 실행
# 헤더파일
<unistd.h>
# 함수 원형
pid_t fork(void);
#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;
}

 

exec() 시스템콜

  • execl(전체 디렉토리와 파일 이름을 합친 이름, argv[0], argv[1], NULL);
  • 기존의 프로세스를 새로운 프로세스 argv( ls -l )로 덮어씌운다 .
  • execl() 시스템콜이 성공하지 못한 경우 다음 코드인 perror() 코드가 실행되어 성공 여부를 알 수 있다. 
    • #include <unistd.h>
      #include <stdio.h>
      #include <stdlib.h>
      int main()
      {
      	printf("execute ls\n");
      	execl("/bin/ls", "ls", "-l", NULL);
      	perror("execl is failed\n");
      	exit(1);
      }

 

  • execlp(환경변수가 설정된 파일 이름, argv[0], argv[1], NULL);
    • execl("ls", "ls", "-l", NULL);
  • execle()
    • // 환경 변수를 지정하고자 할 때
      char *envp[] = {"USER=david", "PATH=/bin", (char *)0};
      execle("ls", "ls", "-al", NULL, envp);
  • execv()
    • // 인수 리스트를 내용으로 하는 문자열 배열
      char *arg[] = {"ls", "-al", NULL};
      execle("bin/ls", arg);
  • execvp()
    • // 환경변수 설정된 파일이름 + 인수 리스트를 내용으로 하는 문자열 배열
      char *arg[] = {"ls", "-al", NULL};
      execle("ls", arg);
       
  • execve()
    • // 환경변수 지정 + 인수 리스트를 내용으로 하는 문자열 배열
      char *envp[] = {"USER=david", "PATH=/bin", (char *)0};
      char *arg[] = {"ls", "-al", NULL};
      execle("ls", arg, envp);

 

wait() 시스템콜

  • wait() 시스템콜을 사용하면, fork() 함수 호출시 자식 프로세스가 종료할 떄까지 부모 프로세스가 기다림 (시그널 사용)
  • 자식 프로세스와 부모 프로세스의 동기화, 부모 프로세스가 자식 프로세스보다 먼저 죽는 경우를 막기 위해 사용 (고아 프로세스)
    • 정상적인 동작은 자식 프로세스가 끝나면 자식 프로세스 정보는 메모리에 남아있고 부모 프로세스가 이를 확인하면 자식 프로세스는 메모리에서 해제된다.
    • 부모 프로세스가 먼저 죽는 경우 확인할 수 없어 자식 프로세스가 메모리에서 해제되지 않는다.
    • 자식 프로세스는 종료되면 좀비 프로세스가 되어 해당 프로세스 조사를 위한 최소 정보만 가지고 있는 상태가 된다.
    • 완전히 끝나면 해당 정보도 삭제되고 부모 프로세스에 SIGCHLD 시그널이 보내진다.
  • wait() 리턴값
    • 에러가 발생한 경우
    • #include <sys/wait.h>
      pid_t wait (int *status) # 리턴값은 종료된 자식 프로세스의 pid
    • status 정보를 통해 기본적인 자식 프로세스 관련 정보를 확인할 수 있음
    • int WIFEXITED(status); - 자식 프로세스가 정상 종료 시 리턴값은 0이 아닌 값이 됨

fork(), execl(), wait() 예시

#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
	int pid;
	int child_pid;
	int status;
	pid = fork(); // 자식 프로세스 생성
	switch (pid) {
	case -1:
		perror("fork failed\n");
		break;
	case 0:
		execl("bin/ls", "ls", "-al", NULL); // 자식 프로세스를 새로 덮어씌움
		perror("execl failed\n");
		break;
	default:
		child_pid = wait(NULL); // 부모 프로세스는 자식 프로세스가 끝날때까지 기다림
		printf("ls is complete\n"); // 자식 프로세스가 끝남
		printf("Parent PID (%d), Child PID (%d)\n", getpid(), child_pid);
		exit(0);
	}
}

 

exit() 시스템콜

프로세스 종료

#include <stdlib.h>
void exit(int status); # status: 프로세스 종료상태 번호

main 함수의 return 0;과 exit(0); 의 차이

  • exit() 함수: 즉시 프로세스를 종료함 (exit() 함수 다음에 있는 코드는 실행되지 않음)
  • return 0: 단지 main() 이라는 함수를 종료함
    • 단 main()에서 return 할때, C언어 실행파일에 기본으로 포함된 _start()함수를 호출하고, 해당 함수는 결국 exit()함수를 호출한다.
    • 따라서 main() 함수에서 return 0;과 exit() 호출은 큰 차이가 없다.

부모 프로세스는 status & 0377 계산 값으로 자식 프로세스 종료 상태를 확인할 수 있다.

  • exit(EXIT_SUCCESS): EXIT_SUCCESS = 1
  • exit(EXIT_SUCCESS): EXIT_SUCCESS = 1 

exit() 시스템콜 주요 동작

  • atexit()에 등록된 함수 실행
  • 열려 있는 모든 입출력 스트림 버퍼 삭제 (stdin, stdout, stderr)
  • 프로세스가 오픈한 파일을 모두 닫음
  • tmpfile() 함수를 통해 생성한 임시 파일 삭제
    • tmpfile(): - 임시파일을 wb+ (쓸 수 있는 이진파일 형대) 모드로 오픈가능

 

 

atexit()

  • 프로세스 종료시 실행될 함수를 등록하기 위해 사용
  • 등록된 함수를 등록된 역순서대로 실행

 

copy-on-write (write시에 복사한다.)

  • fork()는 새로운 프로세스 공간 생성 후, 기존 프로세스 공간 복사
  • 4GB를 복사한다면, 프로세스 생성 시간이 오래 걸림
  • 자식 프로세스 생성시, 부모 프로세스 페이지를 우선 사용
  • 부모 또는 자식 프로세스가 해당 페이지를 읽기가 아닌 쓰기를 할 때 페이지를 복사하고 분리한다
    1. write 요청시
    2. 새로 페이지를 복사하고
    3. page pointer를 변경한다.
  • 장점
    • 프로세스 생성 시간을 줄일 수 있다
    • 새로 생성된 프로세스에 새롭게 할당되어야 하는 페이지 수도 최소화

 

'Computer Science' 카테고리의 다른 글

Pthread  (0) 2022.01.19
쉘 스크립트  (0) 2022.01.14
시스템 프로그래밍 핵심 기술  (0) 2022.01.09
시스템 프로그래밍 - 쉘 사용법  (0) 2022.01.06
시스템 프로그래밍 - 리눅스  (0) 2022.01.03

댓글