Processes in UNIX - 시스템프로그래밍

Processes in UNIX - 시스템프로그래밍

Tag
Computer Science Engineering
System Programming

Process

  • interrupt는 언제든 발생할 수 있다.
 

Context Switch

  • 실행중인 프로세스를 switch 하는 것
  • 현재 실행중인 프로세스 상태 정보를 저장해놔야 한다.
  • 새로 실행 될 프로세스의 정보를 가져옴
  • context switch는 interrupt가 발생되면 일어난다
    • software interrupt (system call) ⇒ 커널이 실행
    • device interrupt ⇒ 사용자 I/O 요청
    • timer interrupt ⇒ (quantum expired) 프로세스가 실행될 때 스케쥴링 알고리즘을 사용할 텐데, 그 중 정해진 시간만큼만 실행해주도록 하는 알고리즘 일 경우
 

Context Switch Steps

  • 프로세스가 interrupt 되었다고 가정할 경우
  1. CPU가 interrupt 발생된 것을 감지한다 ( flag )
  1. CPU는 발생한 interrupt를 처리하기 위해 interrupt handler 루틴을 실행한다 ( privileged mode 즉 커널에서 )
  1. optional( 발생한 interrupt를 처리하는 중에 생긴 또 다른 interrupt를 막을건지 결정 )
  1. interrupt handler는 실행중이었던 process의 state를 저장한다
  1. interrupt handler가 interrupt를 위한 (커널) 코드를 실행
  1. 새로 시작 할 프로세스를 CPU scheduler가 결정
  1. CPU scheduler가 선택된 프로세스의 state를 로드한다
  1. optional ( disable 시킨 interrupt를 다시 enable )
  1. CPU는 다시 user mode로 변경시키고 선택 된 프로세스를 실행시킨다
 

Process identification

  • pid_t getpid(void): 현재 실행중인 프로세스의 id를 리턴
  • pid_t getppid(void): 프로세스의 parent 프로세스 id를 리턴
  • pid_t는 unsigned integer type
 
프로세스는 user ID를 참조할 수 있다.
  • UNIX 내부에서는 real / effective user가 존재한다.
    • getuid ⇒ real user id / getgid ⇒ real group id
    • geteuid ⇒ effective user id / getegid ⇒ effective group id
    •  
real user ⇒ process를 실행시킨 user
effective user ⇒ process가 어떤 유저의 권한으로 리소스를 access 하는가
 
ex) passwd command ⇒ 사용자의 암호를 변경할 수 있음
패스워드를 변경하게 되면 shadow 파일 (중요한 파일이겠징?) 업데이트 ⇒ root만 접근 가능
일반 사용자가 passwd의 프로세스의 effective user ⇒ root 권한
 

Process state

프로세스 생성 ⇒ ready queue로 들어감 ( ready 상태 ) ⇒ CPU에 의해 선택이 되면 running 상태로 변경
⇒ CPU 스케쥴러에 의해 시간이 만료 되면 ready queue로 돌아감 ( 혹은 다른 이유로도 가능하다 )
 
실행상태에서 I/O 요청이 들어온 경우 ( 파일 접근 / IO device의 input , output 요청이 있을 경우 ) ⇒ blocked 상태 ( waiting queue )로 변경 이후 context switch ⇒ interrupt를 통해 I/O가 끝난 것을 알려주면 ready queue로 돌아감
 
💡
blocked 된 이후 바로 running이 되는 것은 아니다. 다시 ready queue로 돌아간 뒤에 실행
notion image
 

Process state

  • ps utility
    • ps 명령어를 사용해서 pid를 파악하기 위한 용도로만 사용
    •  

Process Hierachy

  • 부모, 자식 프로세스
    • A (부모) 프로세스가 자식 프로세스 B 를 만든다.
  • root process
    • 모든 프로세스들의 조상 프로세스
    • 시스템이 부트가 되고 나면 첫번째로 생성되는 프로세스 ( init process )
  • Shell Example
    • 쉘 프로세스를 새로 만든다. ⇒ 사용자가 만든 프로그램을 만든다.
      • 아래의 예시에서는 cat 이라는 프로세스를 만든 것
      • notion image
         

System Function for process

  • fork
    • 부모 프로세스: fork를 호출한 프로세스
    • 자식 프로세스: fork 호출이 됨으로써 새로 생성된 프로세스 / 부모 프로세스의 카피본 ( 100%가 카피되는 것은 아니다 )
    • 새로 생성된 child 프로세스는 0을 리턴받고 그 아래 코드들을 실행한다
      • fork 함수 위는 실행하지 않는다
    • 부모 프로세스는 child 프로세스의 pid를 받고 그 아래 코드들을 계속 실행한다
  • exec family
  • exit
  • wait family
 

fork: Process Creation

  • 프로세스를 새로 만든다
    • 현재의 프로세스 ( 부모 프로세스 ) 정보를 그대로 카피해서 복제본으로서 자식 프로세스를 만든다
  • 부모프로세스 : 호출한 프로세스
  • 자식프로세스 : 새로생성된프로세스
  • return하는 값
    • 호출 시에 자식 프로세스가 생성이 되면서 자식 프로세스의 id 값이 리턴된다.
      • 자식프로세스도 같은 코드를 실행한다.
        • 부모프로세스가 fork()를 실행한 뒤부터 그 아랫줄 부터 코드를 실행한다.
      • 자식프로세스는 0을 리턴받는다.
      • fork함수 자체가 오류가 난 경우 음수가 나온다
 

fork 함수의 특징

  • 새로운 프로세스를 실행한다
    • memory에 있는 부모 프로세스의 image를 카피한다
  • 부모프로세스가 사용하고 있던 일부 env, 권한 정보를 그대로 물려받아서 사용하게 된다.
  • 자식프로세스는 부모프로세스의 일부 리소스를 그대로 상속받아서 사용할 수 있다.
    • ex) 부모가 사용했던 오픈한 파일 / 디바이스
  • 차이점
    • pid ( 자식 프로세스 ) / ppid ( 부모 프로세스 ) 는 다르다
    • 모든 부모프로세스가 사용했던 데이터는 카피된다.
    • 둘 다 같은 코드를 사용한다
    • 같은 지점을 실행하기 시작한다 ( concurrent 하게 )
 

Parent Attributes and Resources

부모프로세스로 부터 상속받지 않는 것
  • Process ID
    • 당연히도 pid는 새로 받는다
  • CPU usage
    • 프로세스가 생성된 순간부터 현재까지 CPU를 얼마나 사용했는지 모니터링 용도로 OS가 가지고 있다.
    • child 프로세스는 당연히도 0일 것이다.
  • Locks and alarm
    • lock ⇒ 여러 프로세스들이 서로 공유하는 리소스를 접근하려는 경우 ( critical section ) conflict가 난다.
  • Pending signals
    • 모든 프로세스는 시그널을 받을 수 있다.
    • 프로세스는 특정 시그널을 받을 건지 안받을 건지 컨트롤 할 수 있다 (시그널 마스크)

  • 자식 프로세스는 어쨌든 독립된 개체이다
  • 프로세스를 더 많이 만들어서 특정 유저가 더 많이 CPU time을 갖게 할 수 있다
 

fork의 장단점

  • 장점
    • 자식프로세스는 부모프로세스로 부터 포크가 될 때 데이터를 상속 받는다.
      • IPC (message queue, shared memory, semaphore 등) 을 안해도 된다!
    • 추가적인 커뮤니케이션을 하지 않아도 된다
      • 오버헤드가 필요 없다
  • 단점
    • 소스코드도 그대로 물려 받기 때문에 자식은 부모와 똑같은 코드를 실행해야 한다
 
notion image
부모의 자원을 그대로 물려받기 때문에 fork시에 자식도 x=0을 가지고 있다
 
argc ⇒ 기본 1 ( 실행 위치 개수 1개 )
argv ⇒ 기본 0 ( 실행 위치 )
 
notion image
상단 상태에서 if(childpid = fork()) 는 현재 실행되고 있는 프로세스가 자식 프로세스를 먼저 만든다.
만들고 나서 childpid가 100이라면 그것은 if(100). 이것은 for문 break로 이어져 fprintf 를 진행하게 된다.
자식프로세스는 if(childpid = fork())를 실행하게 되는데, 이는 if(0) 으로 이어져 그 다음 루프를 진행하게 된다.
 
notion image
ppid가 예상과 다르게 진행된 것은 프로세스의 진행 순서가 예상과 달랐기 때문이다. ⇒ getppid()
부모 프로세스가 자식 프로세스가 돌아가는 동안 살아있어야하는데, 부모가 먼저 종료되었기 때문에 시스템 프로세스가 adopt 하게 된다. 그렇기 때문에 이러한 결과가 나오게 된다 ( 리눅스 os 에서 돌아간 것 )
 
 
notion image
if((childpid = fork()) == -1) ⇒ fork에서 에러가 났을 때만 break가 되게 하면 어떻게 되는가?
 
notion image
 

sleep function

  • sleep(const int second);
    • second 초 동안 프로세스가 동작을 멈춘다
 

wait function

  • 프로세스 중에서 자식 프로세스를 가지고 있는 부모 프로세스가 사용할 수 있다.
    • 부모 프로세스가 자식 프로세스의 종료 여부를 파악해야할 필요가 있을 때 사용한다.
  • wait 함수를 호출하면, 부모프로세스는 실행을 자식 프로세스의 상태가 available ( 자식 프로세스가 종료 될 때 ) 하거나, 시그널을 받을 때 멈춘다.
  • wait 함수를 사용함으로써 부모프로세스가 얻을 수 있는 것
    • 자식 프로세스가 종료되기를 기다릴 수 있다.
    • 종료하는 자식프로세스로부터 status 정보를 받을 수 있다.
    • 자식프로세스가 종료하면서 부모프로세스에게 return 값을 받을 수 있다.
 

wait and waitpid function

#include <sys/wait.h> pid_t wait(int *stat_loc); pid_t waitpid(pid_t pid, int *stat_loc, int options);
  • 자식 프로세스가 어떻게 종료되었는지 stat_loc 을 통해 알 수 있다.
  • 어떤 시그널을 통해 종료가 되었는지
 
  • pid_t waitpid(pid_t pid, int *stat_loc, int options);
    • pid ⇒ 자식 프로세스의 pid ( 보통 0보다 큰 값 ⇒ 자식 프로세스의 값 )
      • 0 / 음수 ⇒ 자식 프로세스를 general하게 쓸 수 있다.
        • 0은 부모프로세스와 같은 프로세스 중 하나라도 종료될 때 까지 기다린다.
        • -1은 자식 중 하나라도 종료될 때 까지 기다린다. ( wait 함수와 같은 기능 )
        • -1을 제외한 음수는 프로세스 그룹을 지정하는 것. (-100 ⇒ 100번 그룹). 100번 그룹 중 자식프로세스 에서 하나를 기다리겠다.
    • options
      • non-blocking 모드로 설정할 수 있다. ( options: WNOHANG )
        • 함수를 호출하고 return 될 때까지 기다리는 것이 일반적이나, non-blocking 모드의 경우 자식프로세스가 끝났는지 확인만 함.
        • 이미 종료된 자식 프로세스의 정보도 알 수 있다.
        • non-blocking 모드로 설정한 경우 자식이 종료되지 않았거나 시그널을 받지 않았다면, 0을 리턴한다
 

stat_loc

  • null이 아니면, 자식 프로세스가 넘겨주는 상태값과 return value를 확인할 수 있다.
  • exit, _exit, _Exit, return 값을 통해서 알 수 있다. ex) return 1 / exti(1)
notion image
 
notion image
 
#include <errno.h> #include <sys/wait.h> pid_t r_wait(int *stat_loc) { int retval; // wait의 리턴값이 -1이고, 에러 코드가 interrupt 이면 (시그널을 받으면) 종료하면 해당 자식 프로세스의 pid를 리턴 while (((retval = wait(stat_loc)) == -1) && (errno == EINTR)); return retval; }
 

exec 함수

→ 다른 코드를 실행한다.
  • fork 함수
    • 호출한 프로세스의 복사본을 생성한다
  • exec 함수
    • 새로운 코드를 로드한다.
      • 현재의 프로세스는 새로운 코드를 실행하는 프로세스가 된다.
      • 새로운 프로세스를 만드는 것이 아니다.
      •  
여러가지 종류의 exec 함수가 있고, 이들은 모두 execve 함수를 호출한다. ( argument가 다름 )
notion image
 

exec

  • 새로운 프로세스가 생성되는 것은 아니지만, 해당 프로세스가 새로운 코드를 실행한다.
  • 정상적으로 실행이 된다면 return 될 일이 없다.
 

exec의 특징

  • program text, variables, stack, heap이 새로 쓰여진다.
  • execle, execve가 아니라면, 환경변수 값은 그대로 사용한다.
 

exec Function Family API

#include <unistd.h> // (char*) 0 => NULL로 대체할 수 있다. // ex) execl("/bin/ls", "ls", "-l", (char*) 0); => /bin/ls위치의 파일을 실행하며 argv는 실행 경로, ls, -l int execl(const char *path, const char *arg0, ..., const char *argn, (char*) 0); int execlp(const char* file, const char *arg0, ..., const char *argn, (char*) 0); int execle(const char *path, const char *arg0, ..., const char *argn, (char*) 0, char *const envp[]); // ex) exev("/bin/ls", ["ls", "-l", (char*) 0 ]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
 

execl

execl(const char *path, const char *arg0, ..., const char*argn, (char*) 0);
  • 미리 결정이 되어있는 arguments와 함께 파일을 실행할 때 ( argument의 개수가 정해져 있지 않다면 )
  • path는 절대경로 / 상대경로. 파일의 경로를 나타낸다.
  • arg0은 path와 같다. ( 그것의 마지막 경로 )
  • arg1 ~ argn은 argument 이다.
  • 0은 argument list의 끝임을 알려준다.
 

execlp

int execlp(const char* file, const char *arg0, ..., const char *argn, (char*) 0);
  • file은 이름만 주어도 가능하다.
  • 환경변수 내부에 PATH 환경변수 중 파악을 하게 됨. 없으면 에러
  • 만약 file에 / 가 포함되어있다면, 이는 execl 처럼 작동하게 된다.
 

execle

int execle(const char *path, const char arg0, ..., const char argn, (char*) 0, char *const envp[]);
  • envp는 실행되는 프로세스에서 사용되는 환경변수를 세팅할 수 있다.
char *env[] = { "USER=user1", "PATH=/usr/bin:/bin:/opt/bin", (char *) 0 };
 

execv

int execv(const char *path, char *const argv[])

execcmd.c example

#include <errno.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char *argv[]) { pid_t childpid; /* check for valid number of command-line arguments */ if (argc < 2){ fprintf (stderr, "Usage: %s command arg1 arg2 ...\n", argv[0]); return 1; } childpid = fork(); if (childpid == -1) { perror("Failed to fork"); return 1; } /* child code */ if (childpid == 0) { // &argv[1] 는 argv[1]의 값이 아닌 주솟값이기 때문에 그 뒤의 값들까지 접근 가능 execvp(argv[1], &argv[1]); perror("Child failed to execvp the command"); return 1; } /* parent code */ if (childpid != r_wait(NULL)) { perror("Parent failed to wait"); return 1; } return 0; }
 
notion image
 
Question
  • execvp 함수에 execcmd ls -l *.c argument를 넘기면 어떻게 되는가?
 
Answer
같은 디렉터리에 있는 .c 파일의 개수에 따라서 argument가 달라진다.
execcmd의 커맨드라인에 넘기기 이전에, *.c의 개수를 파악한다.
 
char* args[] = { “execcmd”, “ls”, “-l”, “a.c”, “b.c”, …, NULL }
 

exit 함수

#include <stdlib.h> void exit(int status);
  • 프로세스를 종료
  • status는 개발자가 정의한 값을 리턴함 ⇒ 부모프로세스가 받는다
 

atexit 함수

프로세스가 정상적으로 종료될 때 수행할 함수가 있다면,
프로세스가 종료되기 이전에 해당 함수를 실행
 
최대 32개의 함수까지 등록할 수 있다.
여러개가 등록된 경우는 스택안에 저장되기 때문에, LIFO 순서대로 실행된다.
 

Background Processes and Daemons

Interrupt character

  • command interpreter로서의 기능을 하는 shell
    • 커맨드를 위해 프롬프트를 띄운다
    • 키보드로부터 사용자가 입력하는 커맨드를 읽는다
    • 자식 프로세스를 fork한 뒤 command를 실행한다
    • 부모프로세스는 자식프로세스를 wait 한다.
 
foreground process를 종료할 때는 ctrl + c

Background process

커맨드를 &를 마지막에 같이 적으면 백그라운드 프로세스로 실행된다.
 

Daemon

background에서 계속 돌아가고 있는 프로세스 ⇒ daemon
setsid를 사용하여, 사용자의 키보드 입력을 받지 않도록 한다.
 
23번 라인 ⇒ 자식프로세스만 실행하는 구문
24번 라인 ⇒ 자식 프로세스가 setsid를 호출한다. 자기만의 세션을 만들고, 자신만의 프로세스 그룹을 만들어서 그 그룹의 리더가 된다. ⇒ 그렇게 되면 controlling terminal을 갖고 있지 않게 된다. ( 터미널로부터 입력을 받지 않는다. )