Process
- interrupt는 언제든 발생할 수 있다.
Context Switch
- 실행중인 프로세스를 switch 하는 것
- 현재 실행중인 프로세스 상태 정보를 저장해놔야 한다.
- 새로 실행 될 프로세스의 정보를 가져옴
- context switch는 interrupt가 발생되면 일어난다
- software interrupt (system call) ⇒ 커널이 실행
- device interrupt ⇒ 사용자 I/O 요청
- timer interrupt ⇒ (quantum expired) 프로세스가 실행될 때 스케쥴링 알고리즘을 사용할 텐데, 그 중 정해진 시간만큼만 실행해주도록 하는 알고리즘 일 경우
Context Switch Steps
- 프로세스가 interrupt 되었다고 가정할 경우
- CPU가 interrupt 발생된 것을 감지한다 ( flag )
- CPU는 발생한 interrupt를 처리하기 위해 interrupt handler 루틴을 실행한다 ( privileged mode 즉 커널에서 )
- optional( 발생한 interrupt를 처리하는 중에 생긴 또 다른 interrupt를 막을건지 결정 )
- interrupt handler는 실행중이었던 process의 state를 저장한다
- interrupt handler가 interrupt를 위한 (커널) 코드를 실행
- 새로 시작 할 프로세스를 CPU scheduler가 결정
- CPU scheduler가 선택된 프로세스의 state를 로드한다
- optional ( disable 시킨 interrupt를 다시 enable )
- 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로 돌아간 뒤에 실행
Process state
- ps utility
- ps 명령어를 사용해서 pid를 파악하기 위한 용도로만 사용
Process Hierachy
- 부모, 자식 프로세스
- A (부모) 프로세스가 자식 프로세스 B 를 만든다.
- root process
- 모든 프로세스들의 조상 프로세스
- 시스템이 부트가 되고 나면 첫번째로 생성되는 프로세스 ( init process )
- Shell Example
- 쉘 프로세스를 새로 만든다. ⇒ 사용자가 만든 프로그램을 만든다.
- 아래의 예시에서는 cat 이라는 프로세스를 만든 것
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 등) 을 안해도 된다!
- 추가적인 커뮤니케이션을 하지 않아도 된다
- 오버헤드가 필요 없다
- 단점
- 소스코드도 그대로 물려 받기 때문에 자식은 부모와 똑같은 코드를 실행해야 한다
부모의 자원을 그대로 물려받기 때문에 fork시에 자식도 x=0을 가지고 있다
argc ⇒ 기본 1 ( 실행 위치 개수 1개 )
argv ⇒ 기본 0 ( 실행 위치 )
상단 상태에서 if(childpid = fork()) 는 현재 실행되고 있는 프로세스가 자식 프로세스를 먼저 만든다.
만들고 나서 childpid가 100이라면 그것은 if(100). 이것은 for문 break로 이어져 fprintf 를 진행하게 된다.
자식프로세스는 if(childpid = fork())를 실행하게 되는데, 이는 if(0) 으로 이어져 그 다음 루프를 진행하게 된다.
ppid가 예상과 다르게 진행된 것은 프로세스의 진행 순서가 예상과 달랐기 때문이다. ⇒
getppid()
부모 프로세스가 자식 프로세스가 돌아가는 동안 살아있어야하는데, 부모가 먼저 종료되었기 때문에 시스템 프로세스가 adopt 하게 된다. 그렇기 때문에 이러한 결과가 나오게 된다 ( 리눅스 os 에서 돌아간 것 )
if((childpid = fork()) == -1) ⇒ fork에서 에러가 났을 때만 break가 되게 하면 어떻게 되는가?
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)
#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가 다름 )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; }
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을 갖고 있지 않게 된다. ( 터미널로부터 입력을 받지 않는다. )