프로세스의 종료
프로세스의 종료는 두가지가 있음
하나는 normal 하나는 abnormal 임
정상 종료는 메인 프로그램에서 return 해서 종료하는 경우, main 아니더라도 exit으로 종료하는 경우
(_exit은 exit이랑 조금 다름)
비정상 종료는 세가지가 있음 abort 시스템콜을 사용하는 경우(core dump해서 종료, 디버깅하는 데 사용),
시그널을 받아서 종료하는 경우, 그리고 스레드에 관련된 것(스레드는 생략)
The exit(3) system call
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
exit의 status는 종료되는 상태를 저장하는 변수임
시스템에서 자동으로 assign해주는 것도 있고 일부는 프로그래머가 넣어주기도 함
그래서 exit은 int라서 4byte임 low order eight bits는 parent에 전달됨
parent는 child가 종료되기 전까지 wait하면서 기다리는데 그 wait(&status)의 status로 전달됨
conventional에서 프로그램이 종료할 때 return이나 exit을 하는데 여기 return value에 대한 규약이 있음
종료할 때 정상적으로 종료를 하면 대개 0을 return 함
무슨 문제가 생겨서 parent에게 알려야 하면 1이나 2를 넣어서 전달함
start up routine에서 main을 실행하고 main의 return값이 exit의 인자로 들어감
exit해서 값을 넣어주면 parent의 wait에서 전달 받음
exit은 library이고, 종료 외에 추가적으로 clean up 작업을 하고 종료함
파일을 open 했는데 close를 안한 것도 다 close해주고 메모리도 deallocate해줌
The atexit(3)
#include <stdlib.h>
int atexit(void (*func)(void));
// returns : 0 if ok, nonzero on error
exit 하면 clean up을 하고 _exit을 함. clean up 하는 것을 유저가 설정해줌
등록된 것 역순으로 실행하는데 최대 32개 까지 등록할 수 있음
메인 프로그램이 있으면 스타트업 코드가 시작됨
그래서 return 없이도 exit이 반드시 호출됨
유저 프로그램이 exit을 실행하면 clean up을 하고 최종적으로 프로세스가 죽음
메인에서 어떤 함수를 call 해서 exit 해도 마찬가지로 죽음
그리고 메인 프로그램에서 start up code를 호출하는데
그 메인에서 return 하면 자동적으로 exit이 실행되면서 끝남
exit 프로그램은 종료하기 전에 먼저 자기 자신의 clean up이 있고 자기 자신 handler 가 있음
등록한 handler를 실행하고 돌아오고 실행하고 돌아오고를 반복하고 exit을 함
유저 함수에서 _exit하면 clean up 안하고 곧바로 exit 함
Synchronizing with children
parent가 fork 하고 child 가 exec 해서 exit 할 때까지 parent가 wait함 - synchronization
$ ls -l 하면 이 쉘이 이 문자열을 읽고 parsing 함
ls가 argv[1]이 되고 -l이 argv2가 됨. 그럼 쉘이 fork 하고 child 가 만들어짐
child는 exec("ls -l")을 실행하는 프로그램이 돼서 exit 할 때까지 실행하고
parent 는 child 가 exit 할때까지 wait 하고 기다림, 끝나면 그 다음 $가 뜸
child가 정상 종료든 비정상 종료든 종료를 하면 parent가 깨어남
child의 모든 open 된 파일 디스크립터를 close하고 메모리를 release함
그리고 커널은 child가 종료한 프로세스 엔트리 중에서 여러가지 child의 pid와 state가 entry에 저장됨
그 프로세스가 죽었을 때 그 프로세스 엔트리는 clear가 되어야 함
clear 하기 위해서 pid가 필요함
status 값은 parent에 전달되고 clear 해야해서 시간 간격이 있음
그래서 parent process가 child process 종료를 확인할 때까지 이 프로세스 엔트리에 대한 정보가 남아있음
status가 그렇게 남아있다가 status를 전달 받으면 clear 시킴
종료한 프로세스의 parent는 wait로 confirm 받음
그래서 parent 에서 wait를 해줘야 프로세스 엔트리가 프로세스 테이블에서 clear 됨
The wait(2) system call
#include <sys/wait.h>
pid_t wait(int *statloc);
//return : child process ID if ok, 0(see later), or -1 on error
*statloc 포인터인 이유는 child 가 exit 할때 넘긴 값을 받으려고 임
return 값은 child가 종료할 때 parent가 깨어나는데, child 가 여러 개면 그 중 어떤 건지 알기 위해서 child pid 반환됨
wait는 child가 running 할 동안 프로세스는 parent process는 잠시 실행을 중단함
child 가 한 개 이상이면 parent의 wait는 child(offspring) 중 어느 거 하나라도 종료하자마자 wait에서 깨어남
wait가 -1을 return 하는 경우는 실패 - parent가 만든 child가 없는 거
child가 없으면 wait할 일이 없는데, wait 해버렸다면 실패한 거임
그래서 error no은 child가 없는데 wait 했다는 의미 ECHILD임
parent는 child 가 여러 개 있을 때 wait 여러번 하면 됨
int stat; //wait에 들어가는 변수
for(int i=0; i<3; i++){ //child 세개 fork
if(fork()==0) { //child 만 수행
sleep(rand()%100); //sleep 시간 랜덤이라서 뭐가 먼저 종료될 지 모름
exit(i);
}
}
wait(&stat);
wait(&stat);
wait(&stat);
while(wait(NULL) != -1);
wait(NULL)에서 null을 넣은 이유는 exit하면서 전달한 값을 안 받는다는 말임
wait의 return 값은 child의 pid인데, -1이면 child가 없어서 false를 반환하고 loop 탈출
int status;
pid_t cpid;
cpid = fork(); /* create new process */
if(cpid ==0){
/* child */
/* do something ... */
} else {
/* parent, so wait for child */
cpid = wait(&status);
printf(“The child %d is dead\n”, cpid);
}
------------------------------------------------------------------
pid_t pid;
int status;
pid = wait(NULL) /* ignore status information */
pid = wait(&status); /* status will contain status information */
main(){ /* status -- 자식의 퇴장(exit) 상태를 어떻게 알아내는지 보여준다 */
pid_t pid;
int status, exit_status;
if((pid=fork()) < 0) fatal("fork failed");
if(pid==0){ /*자식*/
/* 이제 수행을 4초동안 중단시키기 위해 라이브러리 루틴 sleep을 호출 한다 */
sleep(4);
exit(5); /* 0이 아닌값을 가지고 퇴장 */
}
/* 여기까지 수행이 진전된 바 이것은 부모임. 자식을 기다린다. */
if((pid=wait(&status)) == -1){
perror("wait failed");
exit(2);
}
/* 자식이 어떻게 죽었는지 알기 위해 테스트한다 */
if (WIFEXITED(status)){
exit_status = WEXITSTATUS(status);
printf("Exit status from %d was %d\n", pid,exit_status);
}
exit(0);
}
child에서 무조건 exit(5)를 함
단순히 status를 5로 전달받으면 normal한 종료인지 abnormal한 종료인지 알 수 없어서
그 상태에 대한 정보를 테스트하는 매크로 WIFEXITED가 있음
exit으로 종료했으면 true, abnormal termination이면 false를 반환함
The waitpid(2) system call
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
//returns : child process id if ok, 0(see later), or -1 on error
child가 여러 개일 때, wait가 특정 child의 종료를 기다릴 때가 있음
waitpid는 내가 기다리는 child를 pid로 지정할 수 있음
argument의 pid가 -1이라면, 특정 pid를 지정하지 않은 거라서 아무 child의 종료를 기다리는 것임
pid가 0보다 큰 숫자로 지정되면, 특정 pid를 지정한 것이고 그 pid를 가진 child의 종료를 기다리는 것임
pid가 0이면 process group id가 같은, 같은 그룹에 속한 child의 종료를 기다리는 것임
pid가 -1이 아닌 음수면 parent와 다른 process group id에 속한 child의 종료를 기다리는데, 그 child가 속한 gid를 말함
세번째 argument인 option에는 WNOHANG, WCONTINUE, WUNTRACED가 있음
WNOHANG은 wait hang block이랑 비슷함 기다리지 말라는 옵션임
기다리지 않고 곧바로 return함, 지정된 child가 즉각 available(exit해서 status를 넘겨주는 것)하지 않으면 그냥 return
그럼 지정한 pid를 가진 child가 종료해서 return 한건지, 그냥 return 한건지 구분해야하는데 return 값으로 구분함
child가 still running일 때 return 값이 0이고, exit 해서 return 했으면 child의 pid가 return 됨
main(){/*status2--waitpid를 사용하여 자식의 퇴장상태를 어떻게 얻는지 본다*/
pit_t pid;
int status, exit_status;
if((pid=fork()) < 0) fatal("fork failed");
if(pid==0){ /* 자식 */
/* 이제 4초동안 중단시키기 위해 라이브러리 루틴 sleep을 호출한다 */
sleep(4);
exit(5); /* 0이 아닌 값을 가지고 퇴장한다 */
}
/* 여기까지 수행이 진전된 바 이것은 부모임. 따라서 자식이 퇴장했는지 확인한다.
퇴장하지 않았으면, 1초 동안 수면한 후 다시 검사한다. */
while(waitpid(pid, &status, WNOHANG) == 0){
printf("Still waiting... \n");
sleep(1);
}
/* 자식이 어떻게 죽었는지 알기 위해 테스트한다 */
if (WIFEXITED(status)){
exit_status = WEXITSTATUS(status);
printf("Exit status from %d was %d\n", pid,exit_status);
}
exit(0);
}
종료하면서 넘겨준 status에는 값 말고 더 많은 정보가 있음
그 중에서 전달된 값만 추출하는 매크로가 WEXITSTATUS임
wait(2) & waitpid(2)
wait는 parent를 block 하고 무작정 기다리는 것임
waitpid는 WNOHANG 옵션을 주면 block 하지 않게 하고, still running 인지 exit 인지 return 값으로 알 수 있음
child가 running하는 동안 자기가 할 작업을 할 수 있는 시간적 여유를 줌
confirm the exit status
WIFEXITED(status), WEXITSTATUS(status) : 정상종료를 하고 그 status를 알 수 있는 매크로
WIFSIGNALED(status) : status 중 signal을 갖고 있는 것이 있음 그 signal 번호를 알아내는 건 WTERMSIG(status)
WCOREDUMP(status) : 프로세스 종료 시 메모리 내용을 dump 할 수 있는데, 그게 됐는지 알아보는 거
WIFSTOPPED(status) : 프로세스 status가 stop인지 알수 있는 거
WSTOPSIG(status) : stop signal을 알 수 있는 거
WCONTINUED(status) : stop 했다가 resume 했는지 알 수 있는 거
#include <sys/wait.h>
void pr_exit(int status)
{
if (WIFEXITED(status))
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}
Zombie Process
child가 exit하고 종료했는데, parent가 wait로 status를 전달 받으면 child가 깨끗하게 종료됨
parent가 wait를 안하고 있으면 child가 종료하면서 남긴 status값이 process entry에 계속 남음
이 child process는 좀비프로세스가 돼서 ps로 목록을 보면 좀비로 나타남
while(pid=fork()){
if(++count==3) break;
}
if (pid == 0){
sleep(5);
printf("I will be back %d\n", getpid());
exit(0); /* 자식은 먼저 죽는다 */
}
else if(pid > 0){
printf("Im parent %d\n", getpid());
printf("Press any key\n");
getchar(); /* 자식이 죽을 때까지 기다린다 */
}
// child 세개 모두 좀비가 됨
Orphan Process
좀비프로세스는 parent가 살아있는데, wait를 안해주는 경우이고
고아프로세스는 child가 exit하기 전에 parent가 먼저 terminate해버리는 거임
그래서 고아프로세스는 주기적으로 init 프로세스가 고아를 찾아서 입양함
while(pid=fork()){
if(++count==3) break;
}
if (pid == 0){
printf("I will be back %d\n", getpid());
sleep(500);
exit(0);
}
else if(pid > 0){
printf("Im parent %d\n", getpid());
printf("Press any key\n");
getchar(); /* 먼저 엔터를 치고 부모는 종료한다 */
}
child의 sleep(500) 동안 고아프로세스가 되고, 주기적으로 입양하는 init 프로세스에 의해서 입양됨
init 프로세스는 wait를 해주는데, 오버헤드가 너무 커서 자주는 못해줌
그럼 그 동안은 좀비프로세스가 되어 있음
The process-id
프로세스에서 가장 중요한 속성은 pid
#include <unistd.h>
pid_t getpid(void); // returns : process ID of calling process
pid_t getppid(void); // returns : parent process ID of calling process
uid_t getuid(void); // returns : real user ID of calling process
uid_t geteuid(void); // returns : effective user ID of calling process
gid_t getgid(void); // returns : real group ID of calling process
gid_t getegid(void); // returns : effective group ID of calling process
pid가 1인 것은 init 프로세스임. 모든 프로세스의 최고 조상
pid가 0인 것은 스케줄러 프로세스, 종종 swapper로서 알려져 있음
이 프로세스에 대응되는 프로그램은 디스크 상에 존재하지 않음
시스템 프로세스로 커널의 일부분이고 유닉스 시스템을 구동함
static int num = 0;
static char namebuf[20];
static char prefix[] = "/tmp/tmp";
char *gentemp(void){
int length;
pid_t pid;
pid = getpid(); /* 프로세스 식별번호를 얻는다. */
/* 표준 문자열처리 루틴들 <string.h> */
strcpy (namebuf, prefix);
length = strlen(namebuf);
/* 화일 이름에 프로세스 식별번호(pid)를 추가한다. */
itoa (pid, &namebuf[length]); /* filename : /tmp/tmp#pid */
strcat (namebuf, ".");
length = strlen (namebuf);
do{
itoa (num++, &namebuf[length]); /* 접미 번호를 추가한다. */
} while (access(namebuf, F_OK) != -1); /* test for existence of file */
return (namebuf); /* filename : /tmp/tmp#pid.#num */
}
/* itoa --정수를 문자열로 변환한다.*/
int itoa(int i, char *string)
{
int power, j;
j = i;
for (power = 1; j >= 10; j /= 10) /* i = 1234, power=1000 */
power *= 10;
for ( ; power > 0; power /=10)
{
*string++ = '0' + i/power;
i %=power;
}
*string = '\0';
}
Process groups and process group-ids
필요 목적에 따라서 프로세스를 하나의 그룹으로 묶을 수 있음
그 묶인 프로세스의 그룹에 대해서 아이디를 붙인 것을 프로세스 그룹 아이디라고 함
마찬가지로 real group id와 effective group id가 있음
pid_t 데이터 타입을 가짐
#include <unistd.h>
pid_t getpgrp(void); // returns : process group ID of calling process
pid_t getpgid(pid_t pid); // returns : process group ID if OK, -1 on error
/* getpid(0) == getpgrp() */
모든 프로세스에는 유니크한 pid가 있고, 모든 프로세스는 자기가 속한 group이 있음
주로 어떤 한 가지 작업을 하는데 연관이 되어있음 예를들면 파이프로 데이터를 주고 받는 두개의 프로세스
프로세스 그룹에는 여러 개의 프로세스가 있는데 그 중 하나가 리더가 됨
근데 그룹 리더는 프로세스 아이디랑 프로세스 그룹 아이디가 같음
프로세스 그룹 아이디를 return하는 시스템콜에 0을 넣으면, pid가 0인 것은 없어서 자기 자신을 얘기하게 됨
ps를 입력하면 프로세스 리스트를 보여줌
ps를 수행하는 프로세스랑 cat을 수행하는 프로세스의 parent가 bash로 같음
Changing process group
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid); //returns : 0 if ok, -1 on error
parent건 child건 자기가 만든 child의 pgid를 다른 pgid로 바꿀 수 있음, 스스로 자기 group id를 바꿀 수도 있음
parent가 child의 group id를 바꾼다는 것은 다른 그룹으로 독립시킨다는 것임.
그 명령어가 setpgid이고 argument는 두개가 필요함, 독립시킬 때 pid랑 pgid를 갖게 해줌
pgid를 0으로 준다는 것은 자기 자신이 리더인 그룹을 새로 만드는 거임
자기 자신의 그룹 아이디랑 자기가 만든 자식의 그룹아이디만 바꿀 수 있음
보통 fork 해서 child를 만들면 child가 스스로 exec해서 변신하는데, 그 이후에는 parent도 pgid를 바꿀 수 없음
Sessions and session-ids
세션은 프로세스 그룹보다 더 큰 차원, 프로세스 그룹이 여러개 모인 것
우리가 터미널에 처음 login id랑 passwd를 치고 로그인하면 $ 쉘이 시작됨
쉘이 로그인 하면서 만들어진 최초의 프로세스임 이후 명령어치면 명령어가 프로세스가 돼서 만들어짐
이 명령이 fork 해서 child를 또 만들 수 있고, child가 또 child를 만들어서 tree구조로 여러 프로세스가 만들어짐
이 쉘은 터미널에 고정되어 있음, 이 쉘은 맨 마지막에 exit해서 나갈 때까지 작동함
터미널의 쉘부터 프로세스들이 다 파생돼서 나옴 그래서 이걸 controlling terminal이라고 함
이 쉘은 아주 타이트하게 연결됐고 거기서 프로세스들이 점점 새끼를 쳐서 만들어지는 거임
$ cmd & 하면 &가 background에서 running한다는 거임
그래서 원래는 그 child process가 exit할 때까지 parent가 wait하느라고 $이 안뜸
&를 붙이면 background에서 실행하느라 바로 $이 뜸
foreground는 원래 1개밖에 못돌림
#include <unistd.h>
pid_t getsid(pid_t pid); // returns : session leader's process group ID if ok, -1 on error
터미널에서 로그인하고 $이 뜨기 전에 id와 passwd를 확인하는 로그인 프로세스가 있음
로그인을 처리하고 그 프로세스가 fork 해서 쉘을 만들어주는 거임
getsid는 자기가 속한 어떤 세션이 있는데, 그 세션의 sid를 return하는 함수임
프로세스 그룹 아이디가 쉘의 pgid하고 sid하고 같다고 함
데몬 프로세스는 controlling terminal을 갖지 않는 프로세스를 말함
세션 내의 모든 프로세스는 그 세션에 연결된 controlling terminal을 공유한다고 했음
세션 독립해버리면 controlling terminal을 사용할 수 없게 됨
쉘이 exit하고 나서 logout을 하면 쉘이 세션 리더니까 그 세션을 종료하게 됨
그럼 디폴드로 이 세션에서 fork 해서 만들어진 모든 프로세스가 죽음
그런데 세션에서 독립해서 나가면 이 세션하고 무관해서 쉘이 exit해도 안죽음
대신 controlling terminal이 없고 표준 출력이 없음. 영원히 돌게 할 수 있음 telnet, ftpd, cron 등
#include <unistd.h>
pid_t setsid(void); // returns : process group ID if ok, -1 on error
쉘이 처음 만들어지고 이 pid와 같은 그룹에 속한 프로세스들이 있고
그 그룹이 속한 세션이 있을 건데 그 pid, pgid, sid가 다 같음
setpgid(103,0) 이렇게 해서 103번 프로세스의 그룹 리더를 103번으로 바꿀 수 있음
current working & root directory
root directory를 변경하는 명령어가 chroot 임
보통은 루트/ 가 있는데 다른 걸로 변경할 때 사용
#include <unistd.h>
int chroot(const char *path); //return : 0 if ok, -1 on error
int main() {
int pid;
if (chroot("/home/mydir") != 0){ /* ‘/’ == ‘/home/mydir’ */
perror("chroot");
exit(0);
}
if (execl("/bin/bash","bash", NULL) == -1) /* ‘/home/mydir/bin/bash’ */
perror("error");
}
Changing user ID and Group ID
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
// Both return : 0 if ok, -1 on error
Process priorities : nice
priority가 프로세스 마다 있고, 여기서는 nice value임
시스템 값을 보통 0에서 7까지 쓸 수 있음
명령어를 쳐서 background에 돌리기 위해서 $ cc prog & 함
이 때 $ exit 해버리면 background에서 돌리는 게 terminate됨 이때 termination을 원하지 않는다면
아니면 priority를 낮춰주고 싶을 때가 있음
$ no hup nice cc prog.c 2>err & $ exit |
하면 프로그램이 되게 오래 돌아버림
2>err은 에러 출력은 2라는 파일에 하라는 말임
nice를 붙여서 우선순위를 낮춰줌
no hup은 exit이 나오면 이 프로세스에 signal을 보내서 kill을 하는데, 그 signal을 차단하는거임
그래서 죽지는 않지만 priority를 조금 낮춤
'CS > 유닉스프로그래밍' 카테고리의 다른 글
9. 유닉스 시그널 프로세싱 - 2 (0) | 2020.12.07 |
---|---|
8. 유닉스 시그널 프로세싱 - 1 (0) | 2020.12.07 |
6. 유닉스의 프로세스 - 1 (0) | 2020.12.02 |
3-2. 유닉스 파일 엑세스, 파일 시스템, permission (0) | 2020.10.07 |
3-1. 유닉스 파일 : redirection, 표준 IO 라이브러리, error handling (0) | 2020.10.06 |