블로그 이전했습니다. https://jeongzero.oopy.io/
kernel of linux - system call(2)
본문 바로가기
컴퓨터 관련 과목/운영체제 & 커널

kernel of linux - system call(2)

728x90

이번장은 전 강의에 이어서 exec()에대해서 설명한다.

1. fork()


부모 프로세스는 main부터 시작해서 쭉쭉 실행이 되다가 fork()를 호출함으로써 자식 프로세스를 생성한다. 호출이 끝나고 부모는 그대로 진해을 하면서 else로 빠지게 되고, 자식프로세스는 조건문 안으로 들어와, exec 계열의 함수를 실행하게 된다.

리눅스 시스템에서는 현재 수행하고 있는 프로세스를 새로운 프로그램 이미지로 실행시키는 시스템 호출을 제공하고 있다. 전달할 인자에 따라 execl, execv, execle, execve, execlp, execvp 로 나눠진다.

자식 프로세스 로직을 보면, execlp()함수를 호출하게 되는데, 인자로 '/bin/data'를 주게된다. exec 계열 함수가 호출되면 그 즉시 현재의 프로세스의 기본적인 정보(file, mask, pid 등)만 유지한채 '/bin/date' 라는 새로운 실행 프로세스로 교체된다. 즉, fork를 통해 복사한 부모 프로세스의 정보를 가지고 디스크로부터 새로운 이미지를 가져와 복사한 그 위에 그대로 덮은뒤 실행하게 된다는 소리이다.

만약 내가 hello 라는 바이너리를 만들었다고 해보자. hello 바이너리를 실행시키면, 실제 동작과정이 아래의 그림처럼 구성된다.

https://ctf-wiki.github.io/ctf-wiki/executable/elf/running-overview/

./hello를 터미널에서 입력하면, fork() 함수가 호출되면서 자식 프로세스를 생성한다. 그다음 그 자식프로세스에서 execve함수를 호출하여 실제 hello 바이너리를 실행시키면, 이때부터 커널 모드로 넘어가서 전전강의에서 설명한 커널로직이 수행된다.

hello를 gdb로 현재 로드한 상황이다. main 에 bp를 걸고 생성된 프로세스들을 확인해보았다. gdb를 수행한 프로세스는 현재 내 쉘(pid:233)에서 fork가 되서 pid 424를 가지게 된다. 여기서 main까지 실행을 시키면 fork가 또 일어나 자식프로세스가 생성되면서 pid 470인 프로세스에서 hello가 실행되는걸 볼수가 있다.

2. wait()


이번엔 wait() 시스템 콜에 대해서 알아보자. 만약 A라는 프로세스의 로직이 수행되다가 wait() 시스템 콜을 만났다. 그럼 그 즉시 커널 모드로 변경되면서 커널 단에서 sys_wait() 함수의 호출이 일어날것이다. 정상적이라면 커널모드의 동작이 끝난후 다시 유저모드로 되돌아 가야한다.

하지만 wait() 시스템 콜은 다시 A 프로세스의 유저모드로 돌아가지 않는다. wait() 자체가 대기하라는 기능이므로 커널은 A 라는 프로세스로 돌아가지 않고 CPU를 다른 놈에게 줘버린다. 즉 A는 대기 상태가 되고, 예를 들어 B 프로세스에게 CPU를 넘겨주게 된다.

이러한 동작이 가능한, 이유는 커널모드는 어떠한 메모리도 접근할수 있기 때문에 현재 가장 우선순위가 높은 프로세스의 PCB를 가져와서 해당 PC레지스터에 담긴 정보를 토대로 jump를 하게 된다. 위 설명을 코드로 다시한번 살펴보자.

fork() 호출을 통해 자식 프로세스가 생긴다. 우선 부포 프로세스만 봤을때 else로 빠지게 되면서 wait() 함수가 호출된다. 그 즉시, 부모 프로세스는 sleep 상태로 빠지게 되고, CPU는 부모에서 자식에게 간다.

자식 프로세스에서 exec() 계열 함수가 실행되면, 현재 프로세스의 리소스들은 /bin/data 라는 새로운 바이너리를 위한 프로세스의 리소스로 덮히면서 복제된 메모리 영역은 다 해제된다.

그 후에 자식 프로세스의 수행이 끝나면서 특정 시그널을 보내면, 그때야 sleep이 풀리면서 ready 큐에 들어가게 된다. (wait → ready)

3. exit()


내가 짠 소스코드에 exit()를 넣지 않아도 컴파일러가 대부분 마지막에 exit()을 넣어준다. 이는 전에 main 함수의 호출 과정을 정리했던 부분에서 정리한 내용이므로 아래의 게시글에서 확인 가능하다.

main 함수가 호출, 종료되는 과정
elf 파일을 보통 디버깅 할때 main문 부터 확인했었다. 하지만 문제를 풀다가 main함수가 호출되기 까지, 종료된 후 의 과정을 알아야 할 필요가 생겨서 간단하게 정리하고자 한다. 우선 readelf -h 명령어로 현재 challenge 라는 바이너리를 확인한 결과이다. 엔트리 포인트 주소를 보면 0x4006c0 을 가리키고 있는데 해당 주소가 무엇인지 아이다로 확인해보자. 해당 주소는 _start 함수이다.
https://wogh8732.tistory.com/228?category=699165

exec 함수로 date 프로그램을 실행하면, 디스크에서 date 프로그램의 이미지(소스코드로 이해하자) 를 디스크에서 가져와서 실행하고, main문 부터 실행이 된다. 쭉쭉 실행되고, exit() 시스템 콜을 호출하면서 자식프로세스는 종료되고, 그 때 부모가 exit()으로 인한 시그널을 받아 일어나게 되는 것이다.

4. fork, exec, wait, exit 시스템 콜 정리


지금까지 설명한 시스템 콜을 정리해보자.

  • fork

    부모의 리소스를 복제해서 자식을 만든다

  • exec

    복제한 자식위에다가 실행하려는 새로운 이미지를 덮어씌우고 main으로 간다

  • wait

    wait 시스템 콜을 호출한 프로세스를 sleep 시킨다. (자식 프로세스가 끝날때 까지)

  • exit

    내가 가진 모든 리소스를 해제하고, 부모에게 알린다. 이때 위에서 sleep한 프로세스가 깨어나 ready 상태로 된다.

5. Context Switch by wait() & exit()


그렇다면 wait() ↔ exit() 시스템 콜들은 서로 상호작용을 하게 되는 시스템 콜일것이다. 이 두개의 시스템 콜에 의해 어떻게 Context Switch가 일어나는지 자세히 살펴보자.

  1. 유저가 현재 shell에서 'ls' 커맨드를 치게 되면 그 때 fork가 호출되면서 자식 프로세스가 생성되면서 부모 쉘 프로세스의 PCB와 이미지 정보를 복사해온다. 하지만 아직 CPU는 부모에게 있으므로 자식은 실행되지 않았다

  1. 따라서 자식 프로세스(쉘)은 ready 큐에서 대기하고 있다. 부모 프로세스 쉘에서 wait 시스템 콜을 호출한다

  1. 부모가 wait을 호출하게 되면, 그때야 비로소 자식 프로세스 쉘 코드가 실행이 된다. 자식은 execlp함수를 호출하여 '/bin/ls'를 실행시킨다. 즉 디스크로 부터 'ls' 관련 이미지(코드)를 가져와서 현재 쉘 프로세스에 덮어쓴다. 따라서 우측에 나와있는 ls 커맨드 code가 쉘에 올라온다.

  1. ls가 실행되면서 종료될때 exit 시스템 콜을 호출한다.

  1. exit의 호출이 완료되면, CPU를 다른 프로세스에게 전달하고 sleep 되었던 부모 프로세스 쉘을 깨우고 다시 일을 시작한다.

위 과정을 도식화 하면 다음과 같다.

쉘은 유저모드와 커널 모드를 왔다갔다 하면서 context swtiching을 하게 된다.

context swtiching에 대해서 좀더 자세히 살펴보자

Context Switching이란 CPU가 한 개의 Task(Process / Thread) 를 실행하고 있는 상태에서 Interrupt 요청에 의해 다른 Task 로 실행이 전환되는 과정에서 기존의 Task 상태 및 Register 값들에 대한 정보 (Context)를 저장하고 새로운 Task 의 Context 정보로 교체하는 작업을 말한다.

현재 메모리에는 P1, P2, 그리고 커널 프로그램이 들어있다. 커널안에는 하드웨어 장치들의 정보를 담고 있는 Data Structure와 각 프로세스의 정보를 가지고 있는 Dtaa Structure(PCB)가 들어있다.

이런 상황에서 현재 CPU가 P1 프로세스를 실행시키는 도중 P1을 block 시켜야할 때가 되어서 wait syscall을 호출한다. 그러면 커널모드로 변경되게 되고, 커널 안에 존재하는 sys_wait을 호출하면서 현재 P1 프로세스의 CPU state vector (PC, SP, 각종 레지스터들 등 )를 P1의 PCB에 저장하게 된다.

그 다음 커널은 P1이 wait 상태로 됬으므로 커널안에 존재하는 하드웨어 Data Structure 즉, CPU의 리소스 정보를 담고 있는 Data Structure의 ready 큐에서 현재 가장 우선순위가 높을 찾아 CPU 점유를 넘기게 된다.

만약 선택된 프로세스가 P2라면, P2의 PCB로부터 cpu state vector 들을 cpu에 로드시킨다. 그 후에 이제 PC에 저장된 주소로 이동하면서 P2 프로세스가 실행된다. 이 과정이 바로 Context Switching이라고 부른다. 해당 Context Switching을 해주는 놈이 커널안에 schedule() 이라는 함수이다.

6. Context Switch - schedule()


schedule() 함수는 커널 프로그램 내부에 존재하는 함수로써, 유저 단에서 시스템 콜로 커널에게 요청을 통해 호출할수 있는 함수가 아닌, 커널 내부에서만 호출이 가능한 함수이다.

위에서 말한 wait 시스템 콜 뿐만 아니라, read와 exit에서도 context switching이 일어난다. disk를 read하려고 하지만 현재 다른 프로세스가 사용중이라면 대기하면서 현재의 CPU 점유를 양도해야한다. exit도 마찬가지 이다. 이러한 시스테 콜을 통해 다음에 실행시킬 프로세스를 선택하고 context_swtch() 를 call한다.

context_swtch() 함수가 call되면, 현재의 프로세스 정보 즉 CPU state를 대기하려는 프로세스 PCB에 저장을 하고 sleep이 된다. 그리고 선택한 새로운 프로세스의 PCB를 읽어서 CPU state를 로드한다. 그런다음 마지막으로 PC에 담겨있는 주소를 fetch하여 실행이 된다. 여기까지 진행되면 context switching이 끝나게 된다.

즉 context_swtch() ⇒ schedule() 함수는 CPU의 상태가 바뀔때마다 호출되게 된다.

7. 정리


여태 말한 내용을 총정리해보자. 아래 사진은 우리가 리눅스 터미널을 띄우고 쉘에서 ls 명령어를 칠때의 내부 동작과정을 나타낸다.

  1. 터미널을 키게되면 쉘이 나오게 된다. 그리곤 사용자의 입력을 기다리게 된다. 이상태에서 ls를 치게 되면 내 현재 쉘 프로세스가 동작하면서 fork()를 호출하게 된다.

  1. fork() 시스템 콜을 요청하면 커널모드로 넘어가 sys_fork가 호출되면서 현재 쉘 프로세스 이미지(코드)를 그대로 복사하게 된다.

  1. fork의 호출이 끝나면 부모 프로세스 쉘은 다음 로직을 수행하게 되는데 바로 wait()를 호출하게 된다.

  1. wait()를 호출하면 다시 커널 모드로 넘어가게 된다

  1. 커널에서 sys_wait를 호출하면서 현재 부모 프로세스의 리소스 정보 즉, 현재 cpu state machine(레지스터들)을 부모 PCB에 저장을 하게되고 context_switch() 함수를 호출하여 레디큐에 들어있는 프로세스에게 CPU를 양도하게 된다. 그리고 부모 프로세스는 block되어 sleep 상태로 된다.

  1. ready list에 방금 생성한 자식 프로세스만 있다는 가정하게, context switching이 일어나면서 자식 프로세스가 CPU를 양도받아 fork 이후 로직을 수행하게 된다. 즉 현 cpu state가 child 프로세스 정보들로 바뀌게 된다.

  1. 부모의 쉘 프로세스를 복사했으므로 자식 프로세스는 분기문에서 exec로 빠지게 된다. 다시 커널에게 exec 시스템 콜을 요청한다

  1. 커널에선 sys_exec 함수를 호출하면서 현재 디스크에 있는 'ls' 커맨드에 대한 이미지를 가져와

  1. 그대로 자식 프로세스에게 overwirte 한다.

10. 9단계까지 끝났으면, 이제 가져온 ls 이미지(소스코드)에 대한 정보가 자식 프로세스에 덮여져있는 상태이다. 따라서 이제 실제로 PC 를 통해 CPU 명령어 사이클이 동작하고 ls의 main문 부터 차례대로 실행이 된다.

11,12 . 그다음 ls의 동작이 끝나면 exit 시스템 콜을 요청한다.

13. 커널에서 sys_exit을 호출하면서 현재 자식 프로세스를 종료시키고 다른 프로세스에게 CPU를 넘겨주기 위해 context switching이 일어난다. 현재 부모 프로세스가 wait상태이므로 이를 ready list에서 뽑아서 PCB 정보를 CPU state에다가 넣고 context swtiching이 마무리된다.

14. 아까 부모 프로세스는 wait 시스템콜에서 대기상태가 됬으므로 그때의 그상태로 다시 돌아가게 되고, 여기서 다시 유저모드의 wait 시스템 콜 요청때로 돌아간다

15. 마지막으로 부모 프로세스 쉘이 다시 동작하면서 사용자의 입력을 받기 시작한다

8. Process , Context


또한 여태 말한 process, context의 개념을 간단히 정리해보자.

  • 프로그램이 메모리에 올라와 실행되면 프로세스가 된다
  • 제한된 주소 공간을 가지는 실행파일이다.(커널은 제한 x)
  • 스케줄링의 단위이다
  • protection의 단위이다.
  • 자원 할당의 단위이다.
  • 유저모드와 커널모드를 변경하면서 실행된다.

process의 context라는것은 무었일까?

  • user space에서의 context는 text, data, bss, heap, stack 영역을 뜻한다.

    bss, data 영역의 차이는 초기화의 여부이고, 초기화가 되어있으면 컴파일 타임에 공간이 할당되고, 안되어있으면 컴파일 타임이 아닌, 런타임시에 할당이 된다.

  • kernel space에서 context는 PCB(user/proc)와 stack 영역을 뜻한다.
  • 하드웨어에의 context는 PC, SP, flags, 레지스터 들을 뜻한다.

9. Demon


추가적으로 데몬 프로세스에 대해서 설명하겠다. 데몬이란 일반적으로 부팅시에 로딩되는 프로그램을 뜻하는데, 백그라운드에서 계속 상주해 있는다. 사용자로부터 요청이 오면 해당 요청을 처리해주고 다시 sleep모드로 들어가는 동작을 반복한다. 또한 데몬 프로세스의 ppid는 1이다. 데몬 프로세스는 다음과 같은 것들이 있다.

  • httpd web server
  • ftpd ftp server
  • lpd lineprinter spooler daemon
  • pagedaemon


728x90