유닉스 프로그램은 실행파일을 만들면 유닉스 차원에서 부가되는 프로그램의 레이아웃이 있음
프로세스에 대해 설명하기 전에, 프로그램 실행 시 어떻게 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를 새로운 프로그램으로 넘길 수 있다. |
$ ./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 |
가 출력됨
여기서 ./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)으로 메모리가 레이아웃된다. |
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 해줌
이 변수들은 함수에서만 유효하기 때문에 미리 메모리를 잡아두지 않고 호출 시 할당하고 반환 시 해제함
heap은 malloc 시 사용함
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 |
프로그램을 컴파일하고 디스크에 저장할 때는 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 -rwxr-xr-x 1 root root 214604 4월 7 17:00 init (실제 사이즈는 초기값 설정한 게 더 큼) |
프로세스
프로세스는 프로그램이 실행되고 있는 그 순간의 인스턴스를 프로세스라고 함
프로그램은 한번 컴파일하면 고정되어 있고 변하지 않음
프로세스는 실행할 때마다 내용이 변하고 함수를 부르면 지역변수를 할당했다가 반환하면 해제함. 매순간 내용이 다름
변수가 가진 값(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
'CS > 유닉스프로그래밍' 카테고리의 다른 글
8. 유닉스 시그널 프로세싱 - 1 (0) | 2020.12.07 |
---|---|
7. 유닉스의 프로세스 - 2 (0) | 2020.12.03 |
3-2. 유닉스 파일 엑세스, 파일 시스템, permission (0) | 2020.10.07 |
3-1. 유닉스 파일 : redirection, 표준 IO 라이브러리, error handling (0) | 2020.10.06 |
2-2. 유닉스 파일 시스템콜 : open, creat, close, read, write, lseek , dup, fcntl (1) | 2020.10.06 |