본문 바로가기

CS/유닉스프로그래밍

6. 유닉스의 프로세스 - 1

유닉스 프로그램은 실행파일을 만들면 유닉스 차원에서 부가되는 프로그램의 레이아웃이 있음

 

프로세스에 대해 설명하기 전에, 프로그램 실행 시 어떻게 main이 수행되는지 살펴보자
main 함수로 command line arguments 들과 환경변수들이 어떻게 넘어가고, 메모리 레이아웃이 어떻게 구성되는지 살펴보자
커널은 main함수를 호출하기 전에 특별한 start-up routine을 호출한다,
실행프로그램들은 이 start-up routine을 프로그램의 starting address로 명시한다
start-up routine은 커널로부터 command line arguments, environment 값들을 취한다.

main Function

유닉스는 기본적으로 C언어를 이용함

int main(int argc, char *argv[]);

main 함수에 인자로 argc와 argv가 있음

argc는 command line에서 argument의 개수임

argv는 포인터의 배열임 문자열

 

새로운 프로세스를 만들고 exec를 꼭 사용함

프로세스를 만들기 전에 main이 실행되기 전 단계에서 실행되는 특별한 스타트업 코드가 있음

메인 함수가 불려지기 전에 먼저 불러지고 거기서 main을 부름

이런 스타트업 루틴은 커널에서 알아서 처리해줌

스타트업 루틴에서 중요한 부분은 command line argument를 처리해주는 부분

argc와 argv를 setting 해주는 작업과 실행 환경을 스타트업 코드에서 해줌

 

 

프로그램 실행 시 exec를 호출한 프로세스는 command line arguments를 새로운 프로그램으로 넘길 수 있다.
예를 들어 사용자들의 명령을 처리해주는 쉘은 사용자가 입력한 프로그램 뒤에 arguments를 실행 프로그램에 넘긴다.

$ ./echoarg arg1 TEST foo

 

command line에 이렇게 입력하면 argc는 4가 됨

 

#include <stdio.h>
int main(int argc, char* argv[]){
	int i;
    	for (i = 0; i < argc; i++)
    		printf("argv[%d] : %s \n", i, argv[i]);
    	exit(0);
}

argv[0] : ./echoarg
argv[1] : arg1
argv[2] : TEST
argv[3] : foo

가 출력됨

여기서 ./echoarg를 하는 이유는 그냥 echoarg라고 하면 현재 디렉토리에 있다고 하더라도 실행이 안될 수 있음

환경 변수에 path라는 변수가 있는데, 명령을 찾는 순서인 디렉토리의 순서를 path 디렉토리가 setting 해줌

이 때 current working directory path가 지정이 안되어 있으면 그 명령을 찾지 못해서

absolute pathname 이나 relative pathname을 넣어줘야 함

./로 relative pathname 을 넣어준 것임

 

명령을 찾는 pathname이 current working directory에 setting 되지 않아도

relative pathname을 넣어주면 제대로 찾아서 실행시켜줌

 

 


환경변수 리스트는 시스템의 환경변수를 말한다 home, path, shell, user, logname 등등
시스템 사용에 필요한 정보들을 말한다.
환경 변수는 global 변수로 정의되어 있다.
환경변수들에 접근을 용이하게 하기 위해서 getenv와 putenv함수를 제공한다
윈도우와 달리 대부분의 유닉스 시스템은 main 함수의 세번째 argument를 환경변수로서 제공한다
호출 프로세스는 프로그램을 실행하기 전에 실행 프로그램의 환경 변수를 임의로 지정하는 것이 가능하다
환경변수는 다음 파일에서 설정 가능하다 : .profile, .login, .schrc 형식은 [변수명: 값]으로 설정한다.

 

extern char **environ;

for (i = 0; environ[i]; i++)
	printf("env[%d] : %s\n", i, environ[i]);

 

argc와 argv외에 envp가 있음 char**와 같고 stdio.h에 있음

environment는 프로그램이 실행되는 환경변수가 있는데 그 환경변수는 env 명령어를 치면 나옴

대표적으로 home, path, shell, user, logname 등이 있음

문자열이라서 마지막에는 null char가 있음

 

int main(int argc, char *argv[], char *envp[]);

 


Memory Layout a C program

c 프로그램을 컴파일하면 실행파일이 나옴

실행파일이 메모리에 로드될 때 메모리에 어떻게 구조를 갖고 메모리를 차지하는지 보여주는 거임

 

c프로그램은 크게 4개의 영역(text, global, stack, heap)으로 메모리가 레이아웃된다.
여기서 global 영역은 초기화 여부에 따라 initialized data segment와 uninitialized data segment로 나뉜다
initialized data segment의 경우 컴파일 된 실행 모듈의 크기에 포함되지만
uninitialized data segment의 경우 실행모듈의 크기에 포함되지 않는다.
uninitialized data segment는 프로그램 실행 시 커널에 의해 공간이 할당되고 초기화 된다.

text segment는 코드 세그먼트라고도 함. 실행 파일은 기계어로 되어 있음.

 

global 영역은 initialized와 uninitialized가 있음

initialized data segment는 변수와 초기값을 미리 저장해두는 거임 

static이랑 global 변수 초기값을 initialized 에 모아서 메모리를 미리 확보해둠

초기 값이 없는 것들은 uninitialized에 모아서 메모리를 잡아줌 이걸 bss라고도 함(block started by symbol)

초기값이 없는 static과 global을 그렇게 해두는 거임

static변수는 local 변수일지라도 그 storage를 compile 시 미리 잡아 둠

 

stack 부분은 fuction을 call 했을 때 그 함수에 있는 local 변수들이 있음

함수가 return 되면 더 이상 그 로컬변수를 사용하지 않아서 release 해줌

이 변수들은 함수에서만 유효하기 때문에 미리 메모리를 잡아두지 않고 호출 시 할당하고 반환 시 해제함

 

heapmalloc 시 사용

int x[100]이라면 100개의 integer를 프로그램 메모리를 로드할 때 미리 할당함

int *x 면 아직 x에 대한 포인터 이기 때문에 

x[11]이렇게 할 수 있어도 x= (int *) malloc (sizeof(int) 100) 하면 그 다음부터 x[11] 쓸 수 있음

 

 

low address에 프로그램 코드, 그 위에 초기값이 있는 메모리 값이 들어감

그 다음 초기값이 없는 데이터가 들어가고 그 위에 스택과 힙이 늘어났다가 줄어들었다가 할 여유공간을 가짐

 

$ size /usr/bin/cc /bin/sh
  text     data  
bss     dec     hex   filename
 79606     1536   916   82058   1408a   /
usr/bin/cc
619234    21120 18260  658614   a0cb6   /bin/
sh

 

프로그램을 컴파일하고 디스크에 저장할 때는 heap, stack는 저장할 필요 없음

그래서 size를 보면 text, data, bss만 있음

 

/* init.c */
int myarray[50000] = {0};
int main() {return 0;}

/* noninit.c */
int myarray[50000];
int main() {return 0;}

[root@localhost root]# cc noninit.c -o noninit
[
root@localhost root]# cc init.c -o init
[
root@localhost root]# ls -l init noninit

-rwxr-xr-x    1 root     root       214604  4  7 17:00 init
-
rwxr-xr-x    1 root     root        14598  4  7 17:00 noninit

(실제 사이즈는 초기값 설정한 게 더 큼)

[
root@localhost root]# size init noninit
   text    data    
bss     dec     hex filename
    706  200272       4  200982   31116 init
    706     252  200032  200990   3111e
noninit

 


프로세스

프로세스는 프로그램이 실행되고 있는 그 순간의 인스턴스를 프로세스라고 함

프로그램은 한번 컴파일하면 고정되어 있고 변하지 않음

프로세스는 실행할 때마다 내용이 변하고 함수를 부르면 지역변수를 할당했다가 반환하면 해제함. 매순간 내용이 다름

 

변수가 가진 값(data, bss, stack, heap, text) 그리고 실행할 때마다 변수가 갖는 값이 다르고 cpu register 값도 다 다름

프로그램id는 없지만 프로세스id는 있음

 

프로세스는 fork 시스템콜로 프로세스에서 child process를 만듦 

 

$ cat file1 file2

 

cat가 프로그램이고 file1 file2는 argument라고 쉘이 해석함

쉘이 해석하고 fork와 exec를 사용해서 프로세스를 만듦

 

$ ls | wc -l

 

ls와 wc를 중간에 파이프로 연결해서 앞의 내용 출력을 뒤의 입력으로 넣어줌

두 개의 프로세스를 만들어서 중간에 파이프로 연결해주는 것임

각 프로세스에는 pid를 할당해주고, 쉘도 pid가 있음

 

유닉스 프로세스는 다른 프로세스를 시작시킬 수 있다.
이는 프로세스 환경이 디렉토리와 같이 계층적인 구조를 구성하도록 만든다.
최상위에 있는 프로세스를 init프로세스라고 하며, 모든 프로세스의 조상이다.

프로세스 실행환경을 보면 유닉스 모든 프로세스는 fork와 exec로 다른 프로세스를 실행시킬 수 있음

그래서 어떤 프로세스가 어떤 프로세스를 생성하게 되면 parent와 child로 계층이 생김 트리구조가 됨

최상위 가장 중요한 controlling 프로세스가 init 프로세스고 os의 프로세스

모든 프로세스는 이 프로세스로부터 실행되는 것이고 모든 프로세스의 ancestor가 됨


The getpid(2) and getppid(2) system call

#include <unistd.h>
pid_t getpid(void);  // returns : the process ID of the calling process
pid_t getppid(void); // returns : the parent process ID of the calling process

fork 시스템콜로 프로세스를 생성하기 전에 getpid와 getppid로 프로세스의 pid를 알아낼 수 있음

어떤 프로세스에서 getpid를 부르면 함수를 호출한 프로세스의 pid가 뭔지 return 해줌

어떤 프로세스에서 getppid를 부르면 이 함수를 호출한 프로세스의 부모 프로세스 pid를 return 함

프로세스 아이디는 시스템에서 unique 하고 음이 아닌 정수

프로세스가 죽으면 그 pid가 free하게 되고, 다른 프로세스가 그 pid를 사용할 수 있음

 

The fork(2) system call

#include <unistd.h>
pid_t fork(void) ; // returns : 0 in child, process ID of child in parent, -1 on error

fork 가 child process를 만들어주는 시스템콜임

fork를 해서 return 값이 0이면 만들어진 child 프로세스에서 실행되는 것이고

return 값이 0 이 아닌 값이면 parent반환값은 child의 pid일거임

에러일 때는 -1을 반환함

 

fork 하면 생기는 child process는 parent process와 같은 코드, 같은 변수를 가지지만 별개의 프로세스임

child는 parent 완전 복제본이라고 볼 수 있음

parent와 child 는 text segment 코드 부분을 공유

 

 

process A가 parent고 process B가 child라고 하면

physical memory에 parent가 사용하는 부분이 있고 child가 사용하는 부분이 있음

여기서 text 부분은 똑같음 data segment는 다름

 

 

#include <unistd.h>

main(){
	pid_t pid;
    	printf("Just one process so far \n");
    	printf("Calling fork ... \n");
    
    	pid = fork();
    
    	if(pid == 0) printf("I'm the child \n");
    	else if (pid > 0) printf("I'm the parent, child has pid %d \n", pid);
    	else printf("Fork returned error code, no child \n");
}

 

pid_t는 primitive data type 임 

fork 하고 child는 그 다음 줄 부터 시작함

child 는 return 값이 0이고, parent return 값은 child 의 pid 임

parent는 if문 보고 false가 되지만 child는 true라서 if 문 실행함

parent는 else if문 실행함

 

The exec family

fork를 사용해서 child process를 만들면 child process는 parent process와 똑같은 코드를 실행함

새로 만들어진 child가 parent와 다른 일을 하는 프로세스로 만들고 싶음

그럴 때 실행하는 게 exec 시스템콜임 exec에는 여섯가지 버전이 있음

 

execve가 실제로 쓰는 시스템콜이고 나머지 다섯개는 쓰기 좀 편하게 바꾼 거임

단순히 exec는 현재 child process가 실행하는 text, data, heap, stack, bss 등 새로운 프로그램 걸로 바꿔주는 거

많은 유닉스 시스템 구현에서는 이 여섯개 버전이 있는데 이 중 execve만 시스템콜이고 나머지는 라이브러리

 

 

#include <unistd.h>
int execl (const char *pathname, const char *arg0, ... /*NULL*/);
int execv (const char *pathname, char *const argv[]);
int execle (const char *pathname, const char *arg0,.../*NULL*/, char *const envp[]);
int execve (const char *pathname, char *const argv[], char *const envp[]);
int execlp (const char *filename, const char *arg0,.../*NULL*/);
int execvp (const char *filename, char *const argv[]);
//All six return : -1 on error, no return on success

l, le, lp / v, ve, vp 이렇게 있는데 ve가 시스템콜임

 

exec에 l과 v가 붙는 것은 앞에 pathname은 같지만 arg를 나열하는지, array로 들어가는지의 차이가 있음

e가 들어가면 환경변수가 들어감

p가 붙는 것은 pathname이 아니라 filename이 들어감

 

$ echo $PATH

shell에 위를 치면 환경변수를 볼 수 있음

파일 이름의 실행파일을 이 디렉토리에서 먼저 찾고 여기에 있으면 실행하면 되는데,

없으면 bin에서 찾고 없으면 local에서 찾고 그럼 그렇게 찾을 수 없으면 에러나는 거임

이런 순서로  찾기 때문에 pathname이 따로 필요가 없음

 

/* runls --ls를 수행하기 위해서 "execl"을 수행 */

#include <unistd.h>
main(){
	printf("executing ls \n");
    	execl("/bin/ls","ls","-l", (char *)0);
    	/* 만일 execl 이 복귀하면, 호출은 실패한 것임*/
    	perror("execl failed to run ls");
    	exit(1);
}

 

execl 에는 absolute pathname이 들어가고 arg들이 들어감

이걸 실행한 프로세스가 runls를 실행한 프로그램이었다면 이 exec를 실행한 순간

이 프로세스가 ls를 실행하는 프로세스로 변함

프로세스는 변하지 않는데 그 프로세스가 실행하는 프로그램이 runls에서 ls로 변함

ls가 다 끝난다고 해서 돌아오지 않고 ls를 실행하고 그냥 끝남

execl을 실행하는 것이 실패하는 경우 다시 돌아와서 perror 에러가 남

 

/* runls2 --ls 를 수행하기 위해 execv를 사용 */

#include <unistd.h>
main(){
	char *const av[] = {"ls", "-1", (char *)0 };
	execv("/bin/ls", av);
	perror("execv failed");
	exit(1);
}

 

execv에서는 absolute pathname이 들어가고, argument들을 어레이로 다 묶어서 넣음

이 프로그램을 runls2라고 하면 runls2를 실행하다가 자기 자신을 ls를 실행하는 프로세스로 바꿈

ls를 성공적으로 실행하면 돌아오지 않고 ls를 실행하고 끝냄

 

main(){
	char* const argin[] = {"myecho", "hello", "world", (char*)0};
    	execvp(argin[0],argin);
}

/* myecho -- 명령줄 인수를 그대로 출력 */
main(int argc, char** argv){
	while( --argc > 0 ) printf("%s", *++argv);
    printf("\n");
}
$ myecho hello world

 

execvp  첫번째 argument에 filename이 들어감(argin[0])

파일 명은 디렉토리 순서대로 쭉 찾아서 있으면 실행함

myecho는 argc는 3이 될 거고 argv[0] = "myecho", argv[1] = "hello", argv[2] ="world"로 이프로그램이 실행됨


/* runls3 -- 부모프로세스에서 ls를 수행 */
#include <unistd.h>

int fatal (char *s){
	perror(s);
    	exit(1);
}

main(){
	pid_t pid;
    	switch (pid = fork()){
       	case -1: 
            	fatal("fork failed");
        case 0 : /* 자식이 호출 */
            	execl("/bin/ls", "ls", "-l", (char*)0);
            	fatal("exec failed");
            	break;
        default : /* 부모는 자식이 끝날 때까지 일시중단 wait */
            	wait((int*) 0);
            	printf("ls completed \n");
            	exit(0);
}

 

 

fork의 return 값이 0이라는 것은 child 라는 것임 

child는 execl이니까 list가 들어 감 argument를 나열함 ls는 dummy로 있는 거임

ls -l을 실행하는 child process는 이 명령어를 실행하는 프로세스로 변신함

execl이 실패한 경우에는 fatal을 수행함

child process 가 ls -l을 실행하는 동안 parent process 는 wait 시스템콜을 실행함

child가 ls -l을 실행하고 종료하면 parent가 wait에서 깨어나서 실행됨

 


/* docommand -- run shell command, first version */

int docommand(char *command){
	pid_t pid;
    	if((pid = fork()) < 0) return (-1);
        
        if(pid == 0){
        	execl("/bin/sh", "sh", "-c", command, (char*) 0);
            	perror("execl");
            	exit(1);
        }
        wait((int*)0);
        return (0);
}

 

쉘에서 명령어 실행하면 쉘이 parent고 이 쉘이 fork 해서 child를 만듦

명령에 argument가 여러 개 있을 건데 이걸 parsing 해야 됨, 코드에서 parsing은 생략됨

fork 해서 child를 만들고 return값이 음수면 실패해서 끝나고

pid가 0면 child라서 exec해서 입력한 cmd를 실행함


fork, files and data

 

fork는 parent가 open한 모든 파일이 child에서도 open 되어 있음

parent table entry를 보면 각각에 대한 vnode table이 1대1 대응되어 있음

parent가 open하면 child도 그 open한 포인터를 그대로 갖고 있음 offest도 똑같음

 

/* fork는 "data"가 최소한 20문자 이상임을 가정한다 */
main(){
   int fd;
   pid_t pid;
   char buf[10];  /* 화일 자료를 저장할 버퍼 */
   if((fd = open("data", O_RDONLY)) == -1)
      fatal("open failed");
   
   read(fd, buf, 10);   /*화일 포인터를 전진 */
   printpos("Before fork", fd);
   
   switch(pid = fork()) {
   case -1:    /* 오류 */
      fatal("fork failed");
      break;
   case 0:     /* 자식 */
      printpos("Child before read", fd);
      read(fd, buf, 10);
      printpos("Child after read", fd);
      break;
   default:    /* 부모 */
      wait((int *)0);
      printpos("Parent after wait", fd);
   }
}

 

fork 해서 child 만들어지면 부모로부터 상속되는 것들

 

The difference between the parent and child

- fork 하면서 return 되는 값이 다름 : child는 0, parent는 child의 pid

- 각 child 들은 parent가 있는데, 그 parent와 child의 parent 가 같은 수 없음

- 실행시간도 다름 tms_utime, tms_stime, tms_cutime, tms_cstime : child는 모두 0으로 setting 됨

- file loc도 상속받지 않고 pending alarm도 clear됨

 

 exec and open files

#inlucde <fcntl.h>
.
.
int fd;
fd = open(“file”, O_RDONLY);
.
.
fcntl(fd, F_SETFD, 1);	/* close-on-exec 플래그를 on으로 설정 */
fcntl(fd, F_SETFD, 0);	/* close-on-exec 플래그를 off으로 설정 */
res = fcntl(fd, F_GETFD, 0);	/* res==1:on, res==0:off */

parent가 fork 해서 child를 만들면, parent에서 이 전에 파일을 open 해서 얻은 파일디스크립터를 child 도 접근 가능

child가 exec해서 다른 프로그램을 실행해도, parent는 이 값을 그대로 유지하고 있음

 

close-on-exec flag

child가 exec를 실행하면 parent가 open 한 파일디스크립터가 자동으로 close 되는 것

이게 off되면 close 안됨, on 이면 child가 exec할 때 자동으로 close 됨

fcntl.h를 include해서 F_SETFD를 1로해서 on할 수 있음

 

int main(int argc, char** argv)
{
  int fd;
  pid_t pid;
	
  fd = open(“test”, O_RDONLY);
	
  pid = fork();
  
  if(pid == 0){
    execl(“exec",“child",0);
  }
  if(pid<0){
    perror("MAIN PROCESS");
  }
	
  wait(NULL);
  printf("MAIN PROCESS EXIT\n");
  close(fd);

  return 0;
}


/* exec.c */

int main(int argc, char** argv)
{
  if( read(3, buff, 512) < 0){
    perror("EXEC PROCESS");
    exit(1);
  }
	
  printf("EXEC PROCESS EXIT\n”);
	
  return 0;
}

 

 Inherited properties from the calling process