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

kernel of linux - system call(1)

728x90

1. System call


저번 시간에 정리한 시스템콜을 다시 한번 정리해보자. 사진 좌측을 보면, main안에 내가 만든 코드가 있다. add, sub 함수가 있고 그 밑에 printf로 출력하는 함수를 넣어놨다. 이 함수는 내가 만든 함수가 아니라 라이브러리에 존재하는 function이다.

라이브러리 내부에 printf() 라는 함수가 구현되어있는데, 결국 printf함수의 주 목적은 출력, 즉 I/O이다. 멀티 유저 시스템에서는 user mode에선 I/O 가 불가능하므로 시스템 콜을 호출하여 커널에게 작업을 요청해야한다. man 명령어로 알수 있듯이, 3번을 가진 명령어는 라이브러리 함수, 2번은 시스템 콜이라고 했다.

위 사진에서 보면 printf 함수 구현 로직 내부에 결국 I/O 를 하기위해 write라는 wrapper 시스템콜 루틴을 호출하게 되고, 이는 chmodk가 호출됨과 동시에 커널 모드로 넘어가서 커널 내부에 존재하는 sys_write 함수가 호출된다.

1.1 System call wrapper routine


위에서 설명한 printf 함수 내부에 write 함수가 시스템콜을 호출하기 위한 wrapper script 로 구현되어 있다.

포너블 문제를 풀다보면 많이 보이는 코드다.

위 강의자료는 x86 기준이라서 int $0x80; 이렇게 나오는데, x64에서는 syscall 이나 다른 것들로 시스템 콜을 호출하게끔 구현되어있다.

저 int $0x80; 이 호출되는 의미는 trap을 유발시킨다고 보면된다. 이때 비로소 커널모드로 변경되면서 커널에게 동작을 위임하고, 커널은 eax(rax)의 call number를 인덱스로 하여 syscall_table에 들어있는 함수 주소를 찾아서 실행시킨다.

0번이 open, 1번이 close ..등등의 넘버는 컴파일러와 서로 합의된 규칙하에 적용이 된다. 즉, 컴파일러를 쓰는 회사가 결정하는 것이며, 그 컴파일러를 쓰는 회사와 커널과의 호환(?) 이 되야한다는 소리이다.

결국 내가 짠 프로그램을 컴파일시키는 컴파일 타임에

  • print 라이브러리 함수가 있고
  • 그 안에 write wrapper가 있으면
  • write에 해당하는 syscall number를 세팅함

    ⇒ write가 call number 7번이다 ! 라고 하는 것은 컴파일러가 결정하는것

이제 컴파일이 완료되었으면, 런타임시에

  • int $0x80 같은 코드가 호출되면,
  • 트랩이 걸리면서 넘어온 syscall number로 커널이 함수 호출

1.2 Kenrel system call function


하나더 보자면, 커널은 모든 영역을 다 접근할수 있다고 했다. chmodk 가 호출되면서 cpu mode bit이 커널 모드로 변경되고나서, 독립된 커널 프로그램이 수행되는데, 커널은 유저로부터 정보를 받을수도, 혹은 요청한 데이터를 다시 전달할수도 있다.

이러한 전달과정에 필요한 로직도 다 함수로 구현되어 있으며, 위 표에 나와있는 함수들이 바로 그러한 함수들이다.

1.3 Write a New system call?


그렇다면 만약 내가 리눅스 커널에다가 새로운 시스템콜을 추가하고 싶으면 어떻게 해야할까?.

결론은 수동으로 직접 시스템 콜을 추가하는건 추천하지 않는다고 한다. 왜냐하면

예를 들어 pwrite 라는 시스템콜을 만들었다면, 만든 시스템콜에 넘버를 부여해야한다. 하지만 만약 부여했다고 해도, pwrite라는 시스템콜은 내 머신안에서만 동작하는 함수이다.

즉, 다른 환경이나 다른 사람의 pc에서는 적용되지 않는다. 즉 platform에 의존성을 띄기 때문에 직접 추가하는것은 비추천한다고 한다. 그러면 대응방안은 뭐가 있을까

새로운 시스템콜을 만드는 것이 아닌, read, write, ioctl 등을 이용하면 된다. 리눅스에서 파일 시스템은 fd로 관리가 된다. 0,1,2는 표준 입력,에러,출력 으로 고정되어있고 3번부터 100? 정도 까지 파일을 open할수 있다.

하지만 fd 제한을 999까지 만들고 open할때 fd에 999를 주는 식으로 자신만의 fd를 만들고, 이에 해당하는 로직을 수행하게 할수 있다.

2. Process Management


커널이 해주는 가장 중요한 임무는 바로 process management 이다. 아래 사진을 봐보자.

분홍색 부분을 커널이라고 하자. 커널이 1차적으로 해야할 일은 하드웨어를 관리해야하는 일이다. CPU, memory, disk, tty 등의 하드웨어 자원들을 세팅을 시킨다.

1차적인 엄무가 끝나면, 그 이후에 유저 프로그램들을 support 하게 된다.

즉 커널은 하드웨어 자원들, 유저 프로그램들을 지원하는 큰 역할을 하는데, 이를 좀더 효율적으로 관리하기 위해 각각의 하드웨어들마다 Data Structure를 가지고 있다.

여기서 말하는 Data Structure 란, 실제 메모리, 디스크, 디바이스 등의 크기가 얼마인지, 어디서 부터 어디까지 현재 사용되고 있는지 와 같은 정보들을 저장하고 있는 구조라고 생각하면 된다.

또한 현재 돌아가고 있는 프로세스들, 예를 들어 쉘, ppt 와 같은게 띄워져 있다면, 그런 프로세스들을 관리하기 위한 Data Structure 또한 필요한데, 이를 PCB(Process Control Block)라고 부른다.

OS에서는 이러한 하드웨어, 프로세스 등을 관리하기 위해서 필요한 정보를 가지고 있어야하는데 이를 메타데이터라고 부른다. 방금 말한 PCB, Hardware Data structure 등이 바로 메타데이터이다.

2.1 PCB


그렇다면 프로세스를 관리하기 위해서 필요한 PCB같은 메타데이터에는 어떠한 내용이 들어가야 할까?

PCB에는 다음과 같은 정보들이 들어있다.

  • 해당 프로세스의 PID
  • Priority
  • 프로세스 상태
  • 프로세스가 메모리,disk 어디에 올라와 있는지
  • directory ⇒ 현재 실행중인 환경이 어디인지
  • 터미널
  • open files

    리눅스에서는 모든게 파일이라고 했다. I/O 조차도 파일로 간주된다. 여기서 말하는 파일은 sequence of byte 라고 표현할수있다.

    키보드, 모니터 등을 내 프로그램이 시작하기 전에 미리 open file을 해놓는다

  • state vector save area

PCB중에 중요한 것중 하나가 바로 state vector save area 이다.

예시를 들어보자. 지금 내가 만든 프로그램이 동작되는 가운데 디스크에 I/O를 해달라고 요청을 했다. 헌데 현재 디스크가 내 프로그램이 아닌 다른 프로그램의 요청을 처리하고 있기 때문에 끝날때까지 기다려야한다. 따라서 현재 내 프로그램의 PCB는 disk wait 상태가 된다.

그렇다면 현재 CPU는 내 프로그램에서 할일이 없다. I/O가 현재 불가능하여 기다리고 있기 때문에 CPU를 필요로 하는 다른 프로그램에게 CPU를 양도해야한다. 만약 disk wait 상태인 PCB가 disk에 접근이 가능해지면, I/O를 진행하게된다.

I/O가 끝나면 이제 PCB는 wait 상태에서 ready 상태가 된다. 왜냐하면, 아까 CPU를 다른놈에게 양도했기 때문에, CPU를 점유하기 위해 또 대기를 해야한다. 따라서 ready 상태가 된다.

이러한 예시를 바탕으로 다시 봐보자.

현재 내 프로그램이 disk에 I/O를 못하는 상황이라면 CPU를 이용할수도 없으니까 다른놈에게 양도해야한다고 했다. 그렇다면 양도하기전, 현재 CPU안의 모든 데이터값을 저장을 하고 넘겨줘야한다.

이러한 값을 저장하는게 바로 PCB안의 state vector save area 이다. 이 공간은 여러 레지스터들을 저장하고있는 곳이라고 보면 된다.

정리를 하면 위와 같은 그림이다. 학교에서 배웠던, cpu 스케줄링, disk 스케줄링 기법들이 다 여기서 사용되는것같당

3. Creating a child process


컴퓨터를 제일 먼저 부팅하면 커널 프로세스가 올라온다. 터미널이 10개면 각 터미널 마다 쉘을 만들어준다. 그러면 커널 밑에 쉘이 10개가 생성되는데, 그중 하나의 쉘에 ppt를 실행하면 쉘 아래에 ppt가 실행된다. 이렇게 부모 - 자식 관계의 계층관계가 생긴다. 즉, 여기서 말하고 싶은건 반드시 child process를 만드는 일이 생기게 된다는 것이다.

하나의 프로세스가 실행되면, 그 프로세스안에는 user stack이 존재하고, 해당 프로세스 정보를 가지고 있는 PCB, 그리고 저번 시간에 설명한 커널 스택이 존재한다. 만약 trap이 걸려서 system call이 호출되면, 커널 프로세스의 함수를 호출한다.

커널 시스템 콜이 하나만 일어나는것이 아니고 여러번 호출되기 때문에 해당 정보를 유지하기 위한 커널스택이 각 프로세스마다 존재한다는 것을 명심하자.

그럼 이제 child process 를 만드는 과정을 살펴보자. 우선 child process를 만들기 위해서 먼저 process의 정보가 담겨저 있는 PCB를 만들고, 그 PCB에 해당하는 프로세스를 만들어야 한다. 자세한 순서는 아래와 같다.

child process를 만드는 순서

  • step 1 (PCB)

    PCB를 위한 공간을 만들고, 부모 PCB의 정보를 복사해온다. 이로써 부모의 작업 공간이 자식의 작업 공간이 된다. 즉, 부모가 쓰던 리소스들을 자식도 공유하게 된다는 소리이다

  • step 2 (Image)
    1. child process가 올라올 공간을 확보한다. 즉, 메모리 공간을 확보해야한다.
    1. 하드웨어 메모리 리소스마다 Data structure(메타데이터)가 있다고 했다. 그 메타데이터 안에 메모리 전체 크기는 얼마며, 어디서 부터 어디까지가 pid는 누가 쓰고 있고 .. 등등의 정보가 들어있다.
    1. 다시얘기하면 저 메타데이터를 뒤지면 child proess가 들어올 공간을 확보할수 있다는 소리이다.
    1. 공간을 확보하면, 그 후에 부모의 image를 그대로 복사해온다. 즉 부모와 자식은 동일한 코드를 가지게 된다.

  • step 3

    디스크로부터 새로운 이미지를 로드한다. 이 말은 child process를 생성하기 위해 2단계에서 메모리 공간을 확보하고, 초기 값 (부모 이미지)을 세팅한뒤에 실제 디스크에서 원하는 프로그램을 가져오는 로직이라고 보면 될것같다.

    예를 들어 부모 프로세스에서 ppt를 자식 프로세스를 만들고 그 안에 실행시키려고 하면, 1,2단계를 통해 공간을 확보 및 초기 세팅을 하고, 실제 ppt 프로그램을 디스크에서 가져오는게 3단계 라고 보면 될것같다.

  • step 4

    이제 새로 생성한 child process의 PCB를 cpu ready 큐에 넣어놓고, CPU 사용을 위해 대기시킨다. 왜냐하면 CPU는 아직 부모 프로세스가 사용하고 있기 때문이다. (쫌 이따 줄테니까 ready 큐에가서 기달려! 요런 느낌)

이러한 4단계의 step을 2개의 system call로 나눠서 표현할수 있다.

  1. fork ⇒ 1,2단계를 통들어 fork라고 부른다 (부모와 아주 똑같은 놈을 만든다)
  1. exec ⇒ 3,4단계를 통들어 exec라고 부른다. (디스크로부터 이미지를 가져온다)

3.1 fork


아까 fork는 1,2 단계라고 했다. 부모 프로세스의 PCB, 이미지 정보를 그대로 자식 프로세스에게 복사하는게 바로 fork이다. 여기서 중요한 사실이 있다. fork가 호출되고 나면 반환되는 값은 2개이다. 어찌보면 당연한 소리이다.

위 그림을 봐보자. 부모가 fork()를 호출함으로써 자식이 생성된다. 여기서 자식은 부모가 생성한 프로세스이기 때문에 부모의 모든 정보들이 복사되고, 리소스들을 서로 공유하게 된다고 했다.

fork의 호출이 끝나면 부모 프로세스에서 fork()를 호출한 다음의 코드가 수행될것이다. 이것이 첫번째 return이다.

두번째로 자식이 fork로 생성되면, read 큐에 cpu 점유를 기다리고 있게 된다. 그러다가 이제 cpu를 점유하게 되면, 실행이 되는데 여기서 어떤걸 실행하는지가 중요하다. 바로 부모 프로세스의 코드를 그대로 복사했기 때문에 부모 프로세스와 똑같은 코드를 실행한다.

또한 부모의 PCB도 복사해왔기 때문에 CPU의 state vector (PC, SP 등) 도 전부 동일하게 자식도 가지고 있다. 그렇기 때문에 위에서 부모가 fork한뒤 return하면 자식도 역시 return하게 된다는 소리이다. 왜냐하면 state vector들도 전부 부모,자식 둘다 동일하게 가지고 있기 때문이다.

정리하자면, fork를 부모가 한번 호출하고 나면 그대로 다시 부모로 돌아가고, fork가 호출되면서 자식이 생성되고, ready 큐에 들어가 있다가, 자식이 CPU를 점유하게 되면, 부모가 fork하고 났던 그 위치부터 일을 시작하게 된다. 왜냐하면 자식은 부모의 PCB를 그대로 가지고 있기 때문이다.

fork ⇒ 한번은 부모에게 return, 한번은 자식에게 return

단, OS가 한번의 fork로 두번 return했을때의 혼동을 막기위해 서로 다른 return값을 전달해준다. 아래 그림으로 다시 이해를 해보자.

pid = fork() 부분을 봐보자. fork()가 호출되고, pid를 return하는데, 위에서 말했듯이 fork는 2번 return을 한다고 했다.

밑에 분기문을 보면 pid가 0이면 이는 child 프로세스에서 return 했다는 뜻이고, 0이 아니면, 부모 프로세스에서 return 했다는 뜻이다. 이렇게 한번의 fork로 2번의 return이 나오게 된다.

4. 결론


이번장에는 시스템 콜과 child process가 어떻게 생성되는지에 대해서 학습하였다. 다음 장에서는 fork() 에 대한 좀더 자세한 설명을 이어진다. 재밌구만

728x90