Researcher to Developer

copy-on-write, exit(), atexit() 본문

코딩/Basic

copy-on-write, exit(), atexit()

Probe29 2021. 1. 1. 17:09

#copy-on-write

리눅스 프로세스 생성과 관련하여 생성 속도를 높이게 해주는 시스템 콜이다.

해당 기능을 이해 하기 위해서는
1. 리눅스 프로세스 구조와
2. 가상 메모리 시스템 간의 메커니즘 이해가 필요하다.


#리눅스 프로세스 구조와 가상 메모리 시스템

 
리눅스에서는 프로세스당 4GB의 가상 메모리가 할당된다.

3(0xc0)~4(0xff)GB 이 공간은 커널 영역으로 여기에 운영 체제가 들어간다.
0~3(0xc0)GB 이 공간은 사용자 영역으로 실행 파일의 이미지가 들어간다. (TEXT, DATA, BSS, HEAP, STACK )

프로세스 마다 커널 영역이 들어가 있는데
가상 메모리 시스템에 페이징 시스템을 이용하여 
각 프로세스 마다 페이지 테이블이 생긴다. (가상 메모리와 물리 메모리의 주소를 매핑해놓은 정보,테이블)
즉, 커널에 할당한 가상 메모리는 프로세스 간 공유가 가능하다.

 

리눅스 프로세스 구조

 

 



#copy-on-write 기본 개념


fork()는 새로운 프로세스 공간 생성 후, 기존 프로세스 공간 복사한다.
프로세스 기본 공간인 4GB를 복사한다면, 프로세스를 생성하는데 시간이 오래 걸리게 된다.
그 생성 시간을 줄이기 위해 copy-on-write가 등장하게 되었다.

자식 프로세스 생성 시 복사하지 않고 부모 프로세스 페이지를 우선 사용한다.
부모 또는 자식 프로세스가 해당 페이지를 읽기를 할 때는 부모 프로세스 페이지만 사용한다.
그러다가 쓰기(업데이트)를 할 때 부모 프로세스 페이지를 사용하고 있기 때문에 
자식 프로세스를 업데이트하면 부모 프로세스 조차도 업데이트 될 수 있어서
이 때 페이지를 복사하고, 분리한다. (메모리 복사 비용을 줄일 수 있다.)

 

copy-on-write 기법이 적용된 경우, 부모/자식 프로세스는 일부 메모리 공간을 공유할 수 있다.

 

 


#copy-on-write (read 시)
자식 프로세스 생성 시 부모 프로세스 페이지를 우선 사용

1. 부모 프로세스의 일부 공간이 페이지 테이블에 새롭게 페이지가 생성되고 그 페이지에 대한 정보가 물리메모리에 올라가 있는 상태에서


2. fork() 하면 자식 프로세스가 4GB가 생겨야 하는데 각각의 공간 중에 읽기만 해야하는 공간이 있다면


3. 해당 공간을 위해 새로운 페이지를 생성하지 않고 기존의 부모 프로세스에서 사용하고 있는 물리 메모리 주소를 그대로 사용한다.
 

 


#copy-on-write (write 시)
부모 또는 자식 프로세스가 해당 페이지를 읽기가 아닌 쓰기 처음 요청 시 페이지를 복사하거나 분리한다.

 

1. 자식 프로세스의 코드 실행 중 write 요청이 오면


2. 페이지 테이블에서 해당 물리 메모리 공간을 복사해서 새로운 물리 메모리 공간에 데이터를 넣어놓고


3. 페이지 테이블이 가리키던 주소를 새로운 물리 메모리 공간으로 바꾸게 된다. (Page Pointer 변경)

 



#copy-on-write 장점  
프로세스 생성 시간을 줄일 수 있다. 
새로 생성된 프로세스에 새롭게 할당되어야 하는 페이지 수도 최소화한다. 




#exit() 시스템 콜 

프로세스를 종료하는 시스템 콜이다.

 

기본개념
부모 프로세스가 fork() 를 실행해서 프로세스를 복사해서 자식 프로세스를 만들고
그 안에서 exec() 를 실행해서 새로운 실행 파일을 덮어씌워서 자식 프로세스 형태로 만들고
자식 프로세스가 끝날 때까지 부모 프로세스가 기다리다가
자식 프로세스가 어떻게 종료되었는지에 대한 상태 정보(status)를

각 상태에 따라 처리하는 것을 기본적인 구조로 갖고 있다.

사용 예는 다음과 같다.

#include <stdio.h>           // <stdio.h> 라이브러리에 exit 시스템 콜이 들어 있다.
void exit(int status);          // 인자로 프로세스 종료 상태를 나타내는 번호(status)를 넘겨주게 되어있다.
                                   // 인자 번호(status)가 0 일 경우 정상종료를 의미한다.

 

부모 프로세스는 status & 0377(비트연산) 계산 값으로 자식 프로세스 종료 상태 확인이 가능하다.

사용 예는 다음과 같다.

exit(EXIT_SUCCESS);     // EXIT_SUCCESS 는 0 
exit(EXIT_FAILURE);      // EXIT_FAILURE 는 1 

 

 


#main 함수의 return 0;와 exit(0);의 차이는?

exit() 
즉시 프로세스를 종료함 (exit() 함수 다음에 있는 코드는 실행되지 않음) - 비정상 종료 시 사용

return 0;
단지 main()이라는 함수를 종료함
뒤처리 코드가 존재하고 그 마지막에 exit() 가 있음
단, main()에서 return 시, C언어 실행 파일에 기본으로 포함된 _start() 함수를 호출하게 되고
해당 함수는 결국 exit()함수를 호출함

main() 함수에서 return 0;은 exit 호출과 큰 차이가 없음




#exit() 시스템콜 주요 동작

1. atexit() 에 등록된 함수 실행 (프로세스가 종료될 때 실행되어야 하는 함수들을 atexit 함수를 사용하여 등록함)
2. 열려 있는 모든 입출력 스트림 버퍼 삭제 (stdin, stdout, stderr 파일 처럼 다뤄지는 이 세 가지 데이터를 지워준다.) 
3. 프로세스가 오픈한 파일을 모두 닫음
4. tmpfile() 함수를 통해 생성한 임시 파일 삭제
참고 : tmpfile() 임시 파일을 wb+(쓸 수 있는 이진 파일 형태) 모드로 오픈 가능

#include <stdio.h>
FILE *tmpfile(void);

 

 

 

#atexit() 함수
프로세스 종료 시 실행할 함수를 등록하기 위해 사용한다.
등록된 함수를 등록된 역순서대로 실행한다.

atexit() 함수 사용 예는 다음과 같다.

#lnclude <stdlib,.h>              //atexit 를 사용하기 위한 라이브러리
#include <stdio.h>
int main(void) {
       void exithandling(void);
       void goodbyemessage(void);
       int ret;

       ret = atexit(exithandling);
       if (ret != 0) perror("Error in atexit\n");
       ret = atexit(goodbyemessage);
       if (ret != 0) perror("Error in atexit\n");
       exit(EXIT_SUCEESS);
}

void exithandling(void)  {
       printf(exit handling\n");
void goodbyemessage(void)  {
       printf("see you again!\n");
}

// 역순으로 실행하기 때문에 
see you again!
exit handling
이렇게 출력된다.