본문 바로가기

프로그래밍(TA, AA)/C C++

[시스템프로그래밍] 프로세스 정보

이번 절의 목표는 유닉스 시스템에서 프로세스가 무엇인지 이해하고, 함수를 사용해 프로세스의 속성을 검색하며, 프로세스 실행 시간 측정 및 환경 변수를 설정하고 사용할 수 있게 되는 것입니다.



개요


프로세스는 현재 실행 중인 프로그램을 의미합니다. 유닉스 시스템에서는 동시에 여러 프로세스가 실행됩니다. 프로세스는 계속 실행상태에 있지는 않으며, 실행에서 수면, 실행 대기 등 규칙에 따라 여러 상태로 변합니다. 현재 유닉스 시스템에서 실행 중인 프로세스를 확인하려면 ps, prstat, sdtprocess 명령을 사용합니다.


시스템에서는 프로세스를 식별하는 데 프로세스 ID(PID, Process ID)를 사용합니다. 관련 프로세스들이 모여 프로세스 그룹(Process group)을 구성합니다. 세션(session)은 POSIX 표준에서 제안한 개념으로, 사용자가 로그인해 작업하는 터미널(terminal) 단위로 프로세스 그룹을 묶은 것입니다. 프로세스를 식별할 때 사용할 수 있는 PID, 프로세스 그룹, 세션 관련 함수는 아래와 같습니다.


 기능

 함수원형

 PID 검색

 pid_t getpid(void);

 부모 PID 검색

 pid_t getppid(void);

 프로세스 그룹 ID 검색

 pid_t getpgrp(void);

 pid_t getpgid(pid_t pid);

 프로세스 그룹 ID 변경

 int setpgid(pid_t pid, pid_t pgid);

 세션 리더 ID 검색

 pid_t getsid(pid_t pid);

 세션 생성

 pid_t setsid(void);


시간 정보를 이용해 프로세스의 실행 시간을 측정할 수 있습니다. 프로세스의 실행 시간을 측정해 시스템 사용 요금을 결정하는 데 활용할 수 있습니다. 또한 프로그램에서 특히 시간을 많이 소비하는 부분을 찾을 수도 있습니다.


프로세스의 실행 시간은 시스템 실행 시간과 사용자 실행 시간으로 구분할 수 있습니다. 시스템 실행 시간은 프로세스에서 커널의 코드를 수행한 시간이고, 사용자 실행 시간은 사용자 모드에서 프로세스를 실행한 시간입니다. 프로세스의 실행 시간을 측정하려면 times 함수를 사용합니다.


 기능

 함수원형

 프로세스 실행 시간 측정

 clock_t times(struct tms *buffer);


모든 프로세스는 부모 프로세스(parent process)로부터 기본 환경을 물려받습니다. 환경 변수(environment variable)를 사용하면 프로세스의 환경을 설정하거나 설정된 환경을 검색할 수 있습니다. 프로그램에서 환경 변수를 사용하는 방법으로는 전역 변수를 사용하는 방법, getenv 함수를 사용하는 방법, main 함수의 인자로 얻는 방법 등이 있습니다.


 기능

 전역 변수와 함수원형

 환경 설정 관련 전역 변수

 extern char **environ;

 환경 변수 검색

 char *getenv(const char *name);

 환경 변수 설정

 int putenv(char *string);

 int setenv(const char *envname, const char *envval, int overwrite)

 int unsetenv(const char *name);




프로세스의 개념


일반적으로 유닉스 시스템에는 여러 사용자가 접속해 다양한 프로그램을 사용합니다. 한편 유닉스 운영체제 커널 같은 시스템 프로그램도 동작합니다. 이렇게 현재 시스템에서 실행되고 있는 프로그램을 프로세스라고 합니다. 이 절에서는 프로세스의 기본 정의를 살펴보고, 프로세스의 구조를 알아보겠습니다. 또한 프로세스는 실행할 때 고정되어 있지 않고 상태가 변하는데, 상태가 변하는 과정 및 시스템에서 실행되고 있는 프로세스를 검색하는 함수도 살펴보겠습니다.



프로세스의 정의


프로세스는 실행 중인 프로그램을 의미합니다. 프로세스를 프로그램이나 프로세서와 혼동하지 않아야 합니다. 프로세서는 인텔 펜티엄 4나 쿼드코어 등과 같은 중앙 처리 장치(CPU)를 의미합니다. 프로그램은 사용자가 컴퓨터에 작업을 시키기 위한 명령어의 집합으로, C 언어 같은 고급 언어나 쉘 스크립트(shell script) 같은 스크립트 언어로 작성합니다. 고급 언어로 작성한 프로그램은 컴파일러를 통해 시스템이 이해할 수 있는 기계어 프로그램으로 변환해야 합니다. 기계어로 변환된 프로그램을 실행 프로그램 혹은 실행 파일이라고 합니다. 스크립트 언어로 작성한 프로그램은 사전에 실행 파일을 만들어놓지 않고, 실행 시 코드를 읽고 해석해 실행합니다. 이렇게 실행하는 방식을 인터프리트 방식이라고 합니다.


컴파일 방식으로 생성된 실행 파일이든, 인터프리트 방식으로 동작하는 스크립트 파일이든 상관없이 이들을 실행하면 프로세스가 됩니다. 즉, 프로세스는 프로세서가 처리 중인 프로그램을 의미합니다. 프로그램, 프로세스, 프로세서의 관계를 설명하자면, 먼저 프로그램 소스 파일을 작성한 후 컴파일해서 실행 파일을 생성합니다. 실행 파일을 메모리에 적재해서 실행하면 프로세스가 됩니다. 이 프로세스를 처리하는 것이 바로 프로세서입니다.



프로세스의 구조


프로그램을 실행하면 프로세스가 생성되는데, 메모리에 적재된 프로세스는 기본적으로 아래와 같이 메모리가 할당 됩니다.

  • 텍스트 영역: 실행 코드를 저장한다. 텍스트 영역은 프로세스 실행 중 크기가 변하지 않는 고정 영역에 속한다.
  • 데이터 영역 : 프로그램에서 정의한 전역 변수를 저장한다. 전역 변수는 프로그램을 작성할 때 크기가 고정되므로 고정영역에 할당된다.
  • 힙(heap): 프로그램 실행 중에 동적으로 메모리를 요청하는 경우에 할당되는 영역으로, 빈 영역→할당→할당 해제 처럼 상태가 변하는 가변 영역이다.
  • 스택(stack): 프로그램에서 정의한 지역 변수를 저장하는 메모리 영역으로, 지역 변수를 정의한 부분에서 할당해 사용한다.
  • 빈 공간: 스택이나 힙과 같이 가변적인 메모리 할당을 위해 유지하고 있는 빈 메모리 영역이다. 프로세스에 할당된 빈 메모리 영역이 모두 소진되면 메모리 부족으로 프로그램의 실행이 중단될 수도 있다.

프로세스 상태 변화

프로세스가 실행되는 동안 계속 실행 상태에 있는 것은 아닙니다. 프로세스의 상태는 규칙에 따라 여러 상태로 변합니다. 프로세스의 상태 중 중요한 것만 나열하면 아래와 같습니다. 




  1. 프로세스는 먼저 사용자 모드에서 실행한다.
  2. 사용자 모드에서 시스템 호출을 하면 커널 모드로 전환되어 실행된다.
  3. 수면 중이던 프로세스가 깨어나 실행 대기 상태로 전환되면 바로 실행할 수 있도록 준비한다.
  4. 커널 모드에서 실행 중 입출력 완료를 기다릴 때와 같이 더 이상 실행을 계속할 수 없을 때 수면 상태로 전환된다.

프로세스의 상태는 위 그림에 있는 상태 중 하나가 됩니다. CPU는 한 번에 한 프로세스만 실행할 수 있으므로, 실제로는 시스템에서 실행 중인 많은 프로세스 중 하나만 1번이나 2번 상태에 있고, 나머지는 3번이나 4번 상태에 있게 됩니다. 만약 어떤 사건이 발생하기를 기다릴 때, 즉 입출력 완료 혹은 다른 프로세스가 종료하기를 기다릴 때는 프로세스가 잠듭니다. 이를 수면 상태라고 합니다. 수면 상태에서 기다리다가 해당 사건이 발생하면 깨어나 실행 대기 상태로 전환됩니다. 실행 대기 상태는 CPU를 사용할 수 있을때까지 기다리는 상태로, 스케줄링에 따라 실행됩니다. 프로세스의 상태를 적절히 전환하는 일은 커널의 프로세스 관리 기능에 해당합니다.



프로세스 목록 보기

현재 유닉스 시스템에서 실행 중인 프로세스의 목록을 보려면 ps 명령을 사용합니다. 아무 옵션 없이 ps 명령을 사용하면 현재 터미널에서 실행한 프로세스만 출력됩니다. 다음 실행 예는 3번 가상 터미널(pseudo terminal)에서 콘 쉘(ksh, Korn shell) 프로세스와 ps 프로세스가 동작하고 있음을 보여줍니다.


[root@dev-web /]# ps

  PID TTY          TIME CMD

 3910 pts/0    00:00:00 bash

 6726 pts/0    00:00:00 ps


솔라리스에는 ps 명령 외에 프로세스를 검색하는 명령으로 prstat과 GUI 방식인 sdtprocess를 제공합니다. 이외에도 GNU에서 제공하는 공개 명령인 top이 있습니다. 솔라리스용 top 명령은 http://www.sunfreeware.com에서 구할 수 있습니다. 이들 명령은 현재 실행 중인 프로세스를 주기적으로 확인해 출력합니다. 시스템에서 실행 중인 전체 프로세스를 보려면 -ef 옵션을 지정합니다.




프로세스 식별


사용자 계정에서 사용자를 식별하는 번호로 UID가 있는 것처럼, 프로세스도 프로세스를 식별하기 위한 프로세스 ID가 있습니다. 관련 프로세스들이 모여 프로세스 그룹을 구성합니다. 여기서는 프로세스를 식별할 때 사용할 수 있는 PID, 프로세스 그룹 및 세션 관련 함수를 살펴봅니다.


PID 검색

PID는 0번부터 시작합니다. 0번 프로세스는 스케줄러(sched)로, 프로세스에 CPU 시간을 할당하는 역할을 합니다. 0번 프로세스는 커널의 일부분이므로 별도의 실행 파일은 없습니다. 1번 프로세스는 init입니다. 프로세스가 새로 생성될때마다 기존 PID와 중복되지 않은 번호가 할당됩니다. 현재 프로세스의 PID를 검색하려면 getpid 함수를 사용합니다.



PID 검색 : getpid(2)

#include <unistd.h>

pid_t getpid(void);

getpid 함수는 이 함수를 호출한 프로세스의 PID를 리턴합니다.



PPID 검색 : getppid(2)

0번 프로세스를 제외하고, 모든 프로세스에는 자신을 생성한 프로세스가 있는데, 이를 부모 프로세스라고 합니다. 부모 프로세스에도 당연히 PID가 있습니다. 부모 프로세스의 PID를 PPID(Parent Process ID)라고 합니다. 0번 프로세스의 경우 부모 프로세스가 없기때문에 PPID도 0입니다. sched 프로세스에 의해 실행된 프로세스는 PPID값이 0이됩니다.


부모 프로세스의 PID를 검색하려면 getppid 함수를 사용합니다.


#include <unistd.h>

pid_t getppid(void);


프로세스 그룹


프로세스 그룹은 관련 있는 프로세스를 묶은 것으로, 프로세스 그룹 ID(Process Group ID)를 부여받습니다. 프로세스는 프로세스 그룹을 구성하는 멤버가 됩니다. 프로세스 그룹은 BSD 계열 유닉스에서 작업 제어를 구현하면서 도입했습니다. 작업 제어 기능을 제공하는 C 쉘이나 콘 쉘은 명령을 파이프로 연결함으로써 프로세스 그룹으로 묶어 한 작업으로 처리할 수 있습니다.


프로세스 그룹 리더

프로세스 그룹을 구성하는 프로세스 중 하나가 그룹 리더가 되고, 프로세스 그룹 리더의 PID가 PGID가 됩니다. 프로세스의 그룹 리더는 변경될 수 있으며, 리더 프로세스가 변경되면 PGID도 변경됩니다.


만약 pid와 pgid가 같다면 pid에 해당하는 프로세스가 그룹 리더가 됩니다. 만일 pid가 0이면 현재 프로세스의 PID를 의미합니다. pgid가 0이면 pid에 해당하는 프로세스가 그룹 리더가 됩니다.



세션


세션은 POSIX 표준에서 제안한 개념으로, 사용자가 로그인해 작업하고 있는 터미널 단위로 프로세스 그룹을 묶은 것입니다. 프로세스 그룹이 관련 있는 프로세스를 그룹으로 묶은 개념이라면, 세션은 관련 있는 프로세스 그룹을 모은 개념입니다. 프로세스, 프로세스 그룹, 세션의 관계를 그림으로 나타내면 다음과 같습니다. 세션은 프로세스 그룹 단위로 작업 제어를 수행할 때 사용합니다.



세션 검색: getsid(2)

세션에도 ID가 할당됩니다. 세션 ID(session ID)라는 개념은 POSIX 표준에는 없고, SVR4에서 정의한 것입니다. 프로세스가 새로운 세션을 생성하면 해당 프로세스는 세션 리더가 됩니다. 세션 리더의 PID는 세션 ID가 됩니다. 현재 세션의 ID는 getsid 함수로 얻을 수 있습니다. getsid 함수는 POSIX 표준이 아닌 SVR4에서 정의한 함수입니다.

#include <unistd.h>

pid_t getsid(pid_t pid);

getsid 함수는 pid 인자로 지정한 프로세스가 속한 세션의 ID를 리턴합니다. 만일 pid가 0이면 현재 프로세스의 세션 ID를 리턴합니다.


[root@dev-web ch05]# ./ex5_3.out

PID : 11207

PGID : 11207

SID : 8690

[root@dev-web ch05]# ps -ef | grep 8690

root      8690  8685  0 18:36 pts/0    00:00:00 -bash

root     11217  8690  0 19:30 pts/0    00:00:00 ps -ef

root     11218  8690  0 19:30 pts/0    00:00:00 grep --color=auto 8690



세션 생성: setsid(2)

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

pid_t setsid(void);

setsid 함수를 사용하면 새로운 세션을 만들 수 있습니다. setsid를 호출하는 프로세스가 프로세스 그룹 리더가 아니면 새로운 세션을 만들고 세션 리더가 됩니다. setsid 함수를 호출하는 프로세스가 프로세스 그룹 리더면 setsid 함수를 호출하지 않아도 됩니다.



프로세스 실행 시간 측정


시간 정보를 이용해 프로세스의 실행 시간을 측정할 수 있습니다. 프로세스를 실행하면 CPU나 메모리 같은 시스템 자원을 사용하게 됩니다. 프로세스의 실행 시간을 측정해 시스템의 사용 요금을 결정하는 데 활용할 수 있습니다. 또한 프로그램에서 많은 시간을 소비하는 부분을 찾아 개선하는 데도 활용할 수 있습니다. 프로세스의 실행 시간은 times 함수를 사용해 측정할 수 있습니다. 프로세스의 실행 시간은 커널 모드에서 실행한 시간과 사용자 모드에서 실행한 시간을 합해 구할 수 있습니다. times 함수는 커널 모드에서 실행한 시간과 사용자 모드에서 실행한 시간을 구분해서 알려줍니다.



프로세스 실행 시간의 구성


프로세스의 실행 시간은 시스템 실행 시간과 사용자 실행 시간으로 구분할 수 있습니다. 시스템 실행 시간은 프로세스에서 커널의 코드를 수행한 시간으로, 시스템 호출로 소비한 시간을 의미합니다. 사용자 실행 시간은 사용자 모드에서 프로세스를 실행한 시간으로, 프로그램 내부의 함수나 반복문처럼 사용자가 작성한 코드를 실행하는 데 걸린 시간입니다.


프로세스 실행 시간 = 시스템 실행 시간 + 사용자 실행 시간




프로세스 실행 시간 측정


프로세스의 실행 시간을 측정하는 데는 times 함수를 사용합니다. times 함수를 사용하면 프로세스의 실행에 소요된 사용자 실행 시간과 시스템 실행 시간을 알 수 있습니다. 특히 모든 자식 프로세스의 실행 시간을 함께 알 수 있습니다. times 함수는 실행 결과를 tms 구조체에 저장하고 클록 틱을 리턴합니다.


tms 구조체

tms 구조체는 <sys/times.h> 파일에 다음과 같이 정의되어 있습니다.

struct tms {
	clock_t tms_utime;
	clock_t tms_stime;
	clock_t tms_cutime;
	clock_t tms_cstime;
};

tms 구조체의 항목별 의미는 다음과 같습니다.

  • tms_utime: times 함수를 호출한 프로세스가 사용한 사용자 모드 실행 시간
  • tms_stime: times 함수를 호출한 프로세스가 사용한 시스템(커널) 모드 실행 시간
  • tms_cutime: times 함수를 호출한 프로세스의 모든 자식 프로세스가 사용한 사용자 모드 실행 시간
  • tms_cstime: times 홈수를 호출한 프로세스의 모든 자식 프로세스가 사용한 시스템 모드 실행 시간

실행 시간 측정: times(2)
#include <sys/times.h>
#include <limits.h>

clock_t times(struct tms *buffer);

times 함수는 프로세스의 실행 시간을 인자로 지정한 tms 구조체에 저장합니다. times 함수가 알려주는 시간의 단위는 시계의 클록 틱(clock ticks)입니다. 이 클록 틱은 <limits.h> 파일에 다음과 같이 정의되어 있습니다.


#define CLK_TCK ((clock_t)_sysconf(3))   /* 3 is _SC_CLK_TCK */


만약 CLK_TCK의 값이 100으로 검색되었다면, 이를 클록 틱으로 나누면 초 단위로 계산하는 것이 가능합니다. 또한 times 함수는 일정한 기준 시점(일반적으로 부팅 시점)부터 이 함수를 호출한 시점까지 경과한 시간을 클록 틱으로 리턴합니다. 따라서 이 함수를 두 번 호출해서 차이를 계산하면 프로세스가 실행된 전체 시간을 계산할 수 있습니다.


프로세스의 실행 시간을 측정하고 전체 시간, 사용자 모드 실행 시간, 시스템 모드 실행 시간으로 나눠 출력해보겠습니다.


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

int main(void) {
    int i;
    time_t t;
    struct tms mytms;
    clock_t t1, t2;

    if((t1 = times(&mytms)) == -1) {
        perror("times 1");
        exit(1);
    }

    for(i = 0; i < 999999; i++)
        time(&t);

    if((t2 = times(&mytms)) == -1) {
        perror("times 2");
        exit(1);
    }

    printf("Real time : %.1f sec\n", (double)(t2 - t1) / CLK_TCK);
    printf("User time : %.1f sec\n", (double)mytms.tms_utime / CLK_TCK);
    printf("System time : %.1f sec\n", (double)mytms.tms_stime / CLK_TCK);

    return 0;
}



환경 변수의 활용


프로세스가 실행되는 기본 환경이 있습니다. 이 환경은 사용자의 로그인명, 로그인 쉘, 터미널에 설정된 언어, 경로명 등을 포함합니다. 기본 환경은 환경 변수로 정의되어 있습니다. 모든 프로세스는 부모 프로세스에서 기본 환경을 물려받습니다. 쉘은 환경변수를 검색, 추가, 수정할 수 있도록 합니다. 환경 변수는 전역 변수 eviron이나 getenv 함수로 검색하고, putenv 함수로 수정하거나 추가할 수 있습니다.



환경 변수의 이해


환경 변수는 '환경 변수명=값' 형태로 구성되며, 환경 변수명은 관례적으로 대문자를 사용합니다. 환경 변수는 쉘에서 값을 설정하거나 변경할 수 있으며, 함수를 이용해 읽거나 설정할 수도 있습니다.


현재 쉘의 환경 설정을 보려면 env 명령을 사용합니다. env 명령을 실행한 결과는 다음과 같습니다. 언어 설정과 히스토리, 로그인명, 로그인 쉘 등의 정보가 환경 변수에 정의되어 있음을 볼수 있습니다.



[root@dev-web ch05]# env

XDG_SESSION_ID=1642

HOSTNAME=dev-web

SELINUX_ROLE_REQUESTED=

TERM=xterm

SHELL=/bin/bash

HISTSIZE=1000

SSH_CLIENT=121.143.117.245 58686 22

SELINUX_USE_CURRENT_RANGE=

SSH_TTY=/dev/pts/0

USER=root

MAIL=/var/spool/mail/root

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/bin

PWD=/home/unix_system/ch05

LANG=en_US.UTF-8

SELINUX_LEVEL_REQUESTED=

HISTCONTROL=ignoredups

SHLVL=1

HOME=/root

LOGNAME=root

SSH_CONNECTION=121.143.117.245 58686 172.16.15.42 22

LESSOPEN=||/usr/bin/lesspipe.sh %s

XDG_RUNTIME_DIR=/run/user/0

_=/usr/bin/env

OLDPWD=/home/unix_system




환경 변수 사용


프로그램에서 환경 변수를 사용하는 방법으로는 전역 변수를 사용하는 방법, getenv 함수를 사용하는 방법, main 함수의 인자로 받는 방법 등이 있습니다.


전역 변수 environ은 환경 변수 전체에 대한 포인터로, 이 변수를 사용해 환경 변수를 검색할 수도 있습니다.

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

extern char **environ;

int main(void) {

    char **env;

    env = environ;
    while(*env) {
        printf("%s\n", *env);
        env++;
    }

    return 0;

}


main 함수 인자 사용

main 함수는 아무 인자 없이 사용할 수도 있고, 1장에서 다룬 것처럼 인자를 지정해 사용할 수도 있습니다. 유닉스에서는 환경 변수를 다음과 같이 main 함수의 세번째 인자로 지정해 사용할 수 있습니다.


int main(int argc, char **argv, char **envp) { ... }


사용방법은 전역 변수 environ과 같습니다. 



환경 변수 검색: getenv(3)

#include <stdlib.h>

char *getenv(const char *name);

getenv 함수는 인자로 지정한 환경 변수가 설정되어 있는지 검색해 결과값을 저장하고 주소를 리턴합니다. 검색에 실패하면 널 포인터를 리턴합니다.



환경 변수 설정: putenv(3)

#include <stdlib.h>

int putenv(char *string)

putenv 함수를 사용하면 프로그램에서 환경 변수를 설정할 수 있습니다. 설정할 환경 변수를 '환경 변수명=값' 형태로 지정하면 됩니다. putenv 함수는 기존 환경 변수의 값은 변경하고, 새로운 환경 변수는 malloc으로 메로리를 할당해 추가합니다. putenv 함수는 수행을 성공하면 0을 리턴합니다.


putenv 함수로 환경 변수 SHELL의 값을 변경해보겠습니다.



환경 변수 설정: setenv(3)

#include <stdlib.h>

int setenv(const char *envname, const char *envval, int overwrite);

setenv 함수도 putenv 함수처럼 환경 변수를 설정합니다. putenv와 다른 점은 환경 변수와 환경 변수 값을 각각 인자로 지정한다는 것입니다. setenv 함수는 envname에 지정한 환경변수에 envval의 값을 설정합니다. overwrite는 envname의 환경 변수에 이미 값이 설정되어 있을 경우 덮어쓰기 여부를 지정합니다. overwrite 값이 0이 아니면 덮어쓰기를 하고, 0이면 덮어쓰기를 하지 않습니다.



환경 변수 설정 삭제: unsetenv(3)

#include <stdlib.h>

int unsetenv(const char *name);

unsetenv 함수는 name에 지정한 환경 변수를 삭제합니다. name에는 삭제하려는 환경변수명만 지정하면 됩니다. 만일 현재 환경에 name으로 지정한 환경 변수가 없으면 기존 환경을 변경하지 않습니다.