CSE/Linux 검색 결과

4개 발견
  1. 미리보기
    2015.08.17 - Palpit

    [Linux] 3. 리눅스 스케줄링

  2. 미리보기
    2015.08.12 - Palpit

    [Linux] 2-1. 프로세스와 스레드(Process & Thread)

  3. 미리보기
    2015.08.11 - Palpit

    [Linux] 2. 프로세스와 스레드(Process & Thread)

  4. 미리보기
    2015.08.10 - Palpit

    [Linux] 1. 리눅스 활용을 위한 기본 지식

[Linux] 3. 리눅스 스케줄링

2015.08.17 14:32 - Palpit
조회수 확인

리눅스 운영체제는 프로세스나 후에 설명될 스레드에 대해 선점(Preemption) 스케줄링 기법을 제공합니다.


선점 스케줄링은 시분할 시스템에서 한 프로세스의 CPU 독점을 방지하기 위해 주어지는 타임 슬라이스(Time slice)가 소진되었거나, 인터럽트나 시스템 호출 종료 시에 더 높은 우선 순위의 프로세스가 발생하였음을 알았을 때 현 실행 프로세스로부터 강제로 CPU를 회수하여 다른 프로세스에 할당하는 것을 말합니다.


이러한 선점 스케줄링을 사용하는 대부분의 운영체제는 보통 CPU 효율의 극대화, 프로세스에 대한 빠른 응답 및 공평성의 향상 등을 위해 여러 가지 스케줄링 정책과 동적 우선순위 기법을 사용합니다.





3.1 성능 향상을 위한 선점 스케줄링의 고려 사항

 다음은 시분할 선점 스케줄링에서 시스템 효율을 높이기 위한 일반적인 스케줄링 원칙입니다.


 1) 연산 위주 프로세스보다 입출력 위주 프로세스에 높은 우선순위를 줍니다. 이와 같은 우선순위 할당의 이유에는 두 가지가 있습니다. 첫째, 입출력 위주의 프로세스를 먼저 스케줄링하면 이 프로세스는 CPU를 조금만 사용하고 입출력 대기를 위해 CPU를 반환하고 CPU는 다른 프로세스에 할당됩니다. 따라서 결과적으로 CPU와 입출력 장치의 동시 작동(CPU I/O overlap)이 이루어져 자원의 활용도가 증가하게 됩니다. 또 다른 이유는 입출력 위주의 프로세스는 일반적으로 대화형 프로세스여서 빠른 응답을 원하는 경우가 많으므로 우선순위를 높게 할당합니다. 그러나 일반적으로 커널은 실행 프로세스의 성격을 사전에 알 수 없으므로 프로세스들을 실행시켜 가며CPU 활용의 자료를 모아 이를 스케줄링에 점진적으로 반영하게 됩니다.


 2) 실시간 프로세스는 일반적인 프로세스와 대부분의 커널 실행 작업보다도 우선순위가 높습니다. 실시간 프로세스의 실행이 늦는 경우 재앙적 상황이 발생할 수 있기 때문입니다(경성 실시간 프로세스). 멀티미디어 서비스 같은 경우에도 오디오와 비디오 자료의 시간적 동기화가 필요한데, 이러한 시작 제약 때문에 실시간 서비스로 분류되지만, 시간 조건을 어기더라도 재앙적 상황은 발생하지 않으므로 이를 연성 실시간 프로세스라고 합니다.


 3) 타임 슬라이스는 프로세스들의 평균 CPU 반환 시간보다 약간 크게 책정하는 것이 좋습니다. 어느 시스템에서 실행되는 프로세스들의 일반적인 특성이 유사하여 평균 CPU 반환 시간을 알 수 있다면 타임 슬라이스는 이보다 약간 크게 주어야 합니다. 그 이유는 잠시 후에 CPU를 반환할 프로세스에서 CPU를 선점하게 된다면 불필요한 문맥 교환 오버헤드가 생기기 때문입니다.


 4) 일반적으로 프로세스가 시스템 호출을 하여 커널 모드 실행에 있을 때, 프로세스의 대기상태 전이로 CPU를 스스로 반환하기 이전에는 선점이 일어나지 않습니다. 프로세스가 커널 모드 실행 중일 때 타임 슬라이스를 적용이나 기타의 이유로 이를 선점하게 되면 시스템 호출의 재진입에 의한 복잡한 커널 내부적 상호 배제 문제가 많이 발생하기 때문입니다. 그러나 긴급한 실시간 프로세스가 발생하였을 때 이러한 속성은 문제가 되기도 합니다. 커널 내에 프로세스가 진입해 있으면서도 이를 선접할 수 있는 기능을 제공하는 커널을 선점형 커널(Preemptive Kernel)이라 하는데 이러한 기능은 Linux Version 2.6.x 부터 제공되고 있습니다.







3.2 리눅스 스케줄링

 리눅스 스케줄링에서 프로세스(스레드)들은 기본적으로 다음의 스케줄링 그룹으로 구분되는데 이를 스케줄링 정책(policy)라 합니다.


  1) SCHED_OTHERS: 일반적인 사용자 프로세스에 적용되는 스케줄링 정책으로 타임 슬라이스와 커널에 의해 지속적으로 변경되는 동적 우선순위를 사용합니다.


  2) SCHED_FIFO: 긴급한 실시간 프로세스에 사용되는 정책으로 모든 SCHED_OTHERS 그룹보다 높은 고정 우선순위를 가지며 타임 슬라이스의 개념이 없습니다. 타임 슬라이스의 개념이 없다는 의미는 해당 프로세스가 가장 높은 우선순위를 가지고 있을 때, 입출력 요구 등에 의해 스스로 CPU를 반환하기 이전에는 CPU를 계속 사용할 수 있는 것을 의미합니다.


 * FIFO => First In First Out


  3) SCHED_RR: 역시 실시간 프로세스에 사용되는 정책으로 모든 SCHED_OTHERS 그룹보다 높은 고정 우선순위를 가지며, 타임 슬라이스를 줍니다. 같은 우선 순위 등급에서는 타임 슬라이스에 의한 라운드-로빈(Round-Robin) 스케줄링 기법이 적용됩니다. Round-robin 스케줄링은 타임 슬라이스가 다 소진되었을 때 스케줄링 큐의 맨 마지막으로 삽입되어 같은 우선순위의 모든 다른 프로세스의 스케줄링 이후에 CPU를 받게 하는 스케줄링입니다.



 SCHED_FIFO와 SCHED_RR의 프로세스는 슈퍼 유저(Super User) 만이 생성할 수 있고, 고정 우선순위를 사용하므로 스케줄링 기법 상으로는 간단한 모델입니다. 일반적 사용자 프로세스에 적용되는 SCHED_OTEHRS 기법은, 앞서 설명한 연산 위주 프로세스와 입출력 위주 프로세스 중 후자에 우선순위를 높게 주는 정책을 반영하기 위한 것 입니다. 모든 프로세스는 생성 시에 타임 슬라이스를 할당받고 이는 기본 우선순위의 역할을 합니다. 이러한 동적 우선순위 시스템을 이해하기 위해 리눅스 스케줄러의 SCHED_OTHERS 프로세스에 대한 우선순위 계산 식과 타임 슬라이스 소진 시의 새로운 타임 슬라이스를 설정하는 2.4.x 경우의 계산식을 알아보겠습ㄴ니다.


 SCHED_OTEHRS의 경우 타임 슬라이스가 소진되지 않은 새로운 프로세스에 스케줄을 줄 때 프로세스들의 우선순위(goodness) 계산 식은 다음과 같습니다.(단 SCHED_FIFO나 SHCED_RR의 실시간 프로세스는 주어진 고정 우선순위를 갖습니다)



   동적 우선순위 = 남은 타임 슬라이스 + (20 - nice)의 비례 값;


 SCHED_OTHERS 프로세스의 타임 슬라이스 소진 시 새로운 타임 슬라이스 설정은 다음과 같습니다. 한 프로세스의 타임 슬라이스가 소진되면 한 프로세스의 남은 타임 슬라이스를 나타내는 태스크 구조체 내의 counter의 값은 0이 되고, 다른 모든 프로세스의 타임 슬라이스가 소진되기까지 이 값은 변화가 없고 따라서 스케줄링을 받지 못합니다. 모든 프로세스의 counter 값이 0이 되면 각 프로세스의 타임 슬라이스를 재 설정하게 되는데 그 방식은 아래와 같습니다.



   새로운 타임 슬라이스 = (20 - nice)의 비례 값 + 1;



 위에서 타임 슬라이스 소진 이전에 여러 가지 이유로 CPU를 선점당하는 경우에, 그 우선순위는 남은 타임 슬라이스에 의존함을 알 수 있습니다. 즉, 타임 슬라이스가 많이 남은 경우네는 CPU를 덜 사용한 것이 되고, 이 경우에는 높은 우선순위를 줌을 알 수 있습니다.


 위의 우선순위 계산 식과 새로운 타임 슬라이스 설정 식에서 nice 값은 우선순위 계산 시의 벌점과 같이 작용합니다. 즉, nice 값이(-20 ~ 20, 기본 값은 0) 작을수록 동적 우선순위 시스템에서 높은 우선순위와 큰 타임 슬라이스를 가집을 알 수 있습니다. SCHED_OTHERS 프로세스의 경우, 사용자는 직접 우선순위를 조정할 수 없고 단지 nice 값을 상향 조정할 수 있을 뿐입니다. nice를 하향 조정하려면 슈퍼 유저의 권한이 필요합니다. fork 시에 nice 값은 상속받게 됩니다. Linux 2.6.x 이상의 버전에서 SCHED_OTHERS는 CFS(Completely Fair Scheduling) 정책이 사용되고 있는데, 기본적인 개념은 그 이전의 버전과 같은 것으로 보아도 무관합니다.


 



 3.2.1 스케줄링 관련 시스템 호출

  프로세스의 우선순위나 스케줄링 정책 조정을 위한 함수들은 다음과 같습니다. 

 

  우선 생성된 프로세스에 대해 일반적인 시분할 프로세스인 경우 SCHED_OTHERS로 설정하고, 실시간 프로세스의 경우에 SCHED_FIFO와 SCHED_RR 정책과 함께 우선순위를 줄 수 있는 함수들은 다음과 같습니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SETSCHEDULER(2)
#include <sched.h>
 
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p);
 
int sched_getscheduler(pid_t pid);
 
struct sched_param {
    ...
    int sched_priority;
    ...
};
 
 
    입력 값
        - pid: 해당 프로세스 id(0인 경우 자신)
        - policy: SCHED_OTEHRS, SCHED_FIFO, SCHED_RR
        - sched_param: policy에 따라 구조체 내부의 sched_priority 값을 설정한다. SCHED_OTHERS인 경우 0을 주어야하며,
                            실시간 프로세스의 경우는 1 - 99 범위의 값을 준다. SCHED_FIFO와 SCHED_RR은 수퍼 유저만 사용할 수 있다.
        - sched_getscheduler는 policy를 리턴한다.
        
    반환 값
        - 정상: sched_setscheduler -> 0, sched_getscheduler -> policy
        - 에러: -1
cs











  위의 설명에서 알 수 있듯이 이 sched_setscheduler 함수는 SCHED_OTEHRS 일 경우는 무의미합니다. 따라서 이 함수는 실시간 프로세스를 지정하고 고정 우선순위를 주는 데 사용합니다.


  스케줄링 정책을 준 실시간 프로세스에 대해 사용자 임의로 우선순위 조정을 위해 사용하는 함수들은 다음과 같습니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SCHED_SETPARAM(2)
#include <sched.h>
 
int sched_setparam(pid_t pid, const struct sched_param *p);
int sched_getparam(pid_t pid, struct sched_param *p)
 
    입력 값
        - pid: 해당 프로세스 id (0인 경우 자신)
        -sched_param: policy에 따라 구조체 내부의 sched_priority 값을 설정한다. 
                        실시간 프로세스의 경우는 1 - 99 범위의 값을 준다.
                      SCHED_FIFO와 SCHED_RR은 수퍼 유저만 사용할 수 있다.
        -sched_getscheduler는 sched_param 구조체를 반환한다.
        
    반환 값
        - 정상: 0
        - 에러: -1
        
cs




  기타 스케줄링 관련 함수들로는 다음과 같은 것들이 있습니다.



1
2
3
4
SCHED_YIELD(2)
#include <sched.h>
 
int sched_yield(void);
cs




  sched_yield 함수는 커널에 재스케줄링을 요구하여 호출 프로세스는 같은 우선 순위 등급의 큐에서 가장 뒤로 옮겨집니다. sched_yield 후에, 자신보다 높은 우선순위 위의 프로세스 없으면 호출 프로세스가 다시 스케줄을 받습니다. 이와 같은 함수는 어떠한 프로세스가 어떤 조건이 만족하길 기다리는 loop을 수행할 때, 조건이 만족하지 않아도 주어진 타임 슬라이스 동안 계속 loop을 수행할 때, 조건이 만족하지 않아도 주어진 타임 슬라이스 동안 계속 loop을 돌아 CPU 시간을 낭비하는 것을 방지하는 데 사용되는 것이 보통입니다. 즉, 한 번의 조건 테스트로 기다리는 조건이 만족하지 않음을 알았을 때, 다른 프로세스에 스케줄을 일단 양보하고 조건 테스트는 후에 다시 스케줄을 받았을 때 다시 하기 위한 함수입니다. 사용 예는 다음과 같습니다.



1
2
3
4
while (!condition) { // 조건은 trylock 이나 세마포어의 try wait 등을 예로 들을 수 있다.
    sched_yield();
    
}
cs



  그 외 SCHED_OTEHRS와 SCHED_RR 정책 프로세스의 nice 값을 조정하는 함수는 다음과 같습니다.



1
2
3
4
NICE(2)
#include <unistd.h>
 
int nice(int inc);
cs




  inc 값이 음수이면 우선순위가 증가하게 되는데, 이는 슈퍼 유저만이 할 수 있습니다. nice 함수는 SCHED_OTEHRS 프로세스의 nice 값을 조정하고 이는 타임 슬라이스와 우선순위 조정에 영향을 미칩니다. 단, SCHED_RR의 실시간 프로세스인 경우는 타임 슬라이스 조정에만 영향을 주고 우선순위는 고정이므로 영향을 미치지 않습니다.


  위에서 설명한 스케줄링 관련 함수들은 일반적으로 nice를 제외하고는 모두 SCHED_FIFO나 SCHED_RR의 실시간 프로세스를 위해 사용되는 것들 입니다. 즉, 본 장의 내용은 실시간 시스템 응용을 작성하는 사용자를 위한 것이라 할 수 있습니다.



다른 카테고리의 글 목록

CSE/Linux 카테고리의 포스트를 톺아봅니다
조회수 확인

 2.3 시스템 호출과 프로세스의 상태 전이

  본 절에서는 일반적인 입출력 시스템 호출 및 인터럽트 처리와 관련된 프로세스의 상태 변이를 알아보기 위해 프로세스의 시스템 호출부터 복귀까지의 과정을 입출력인 디스크 파일 입력의 예로 설명하도록 하겠습니다.


  사용자가 요구한 입출력이 완료되기까지는 커널 내의 관련 시스템 호출 처리 루틴뿐만 아니라 디바이스 드라이버의 주축인 인터럽트 처리 루틴이 서로 유기적인 관계를 맺고 동작합니다. 이 과정에서는 입출력 완료 대기에 의한 프로세스 대기 상태로의 전이, 문맥 교환이 발생하고, 디스크 입출력을 최소화하기 위한 커널의 버퍼링 기법도 등장합니다. 이러한 처리 과정은 커널의 두 가지 대표적 구성 요소인 시스템 호출과 인터럽트 처리의 관계를 이해하는 중요한 부분이 됩니다.




  1) 예제 프로그램

   이 예제에서 사용자 프로세스는 디스크 파일을 열고(open), read 시스템 호출을 수행합니다.


   read 시스템 호출이 수행되면 사용자 프로세스와 CPU는 커널 모드 실행으로 바뀌며 커널 안의 read 기능 실행 함수로 진입하게 됩니다. 커널 내부 함수 진입은 CPU의 모드를 커널 모드 실행으로 바꾸어 여러가지 특권을 갖게 해야 하므로 일반적인 사용자 모드에서의 함수 호출 기법이 아닌 트랩(Trap)이라는 특수 명령어를 수행함으로써 이루어집니다.



* 사용자 프로세스


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
#define BUF_LENGTH 256
 
int main(void) {
    int file_id;
    char buffer[BUF_LENGTH];
    
    file_id = open("data_file"0);    // read-only-mode open
    
    n = read(file_id, buffer, BUF_LENGTH);
    /* 
     * 사용자 모드에서 실행 중 시스템 호출을 하여 커널로 
     * 진입하며 커널 모드실행으로 전환 
     * 복귀 시는 다시 사용자 모드
     * 
     */
  .....
}
cs





  2) 커널의 시스템 호출 처리 루틴

  디스크 파일 입력을 위한 시스템 호출 처리 루틴은 파일 시스템 및 매체의 종류, 가상 메모리 및 페이지 버퍼 관리 등과 연결되어 매우 복잡한 구조를 가집니다. 본 적에서는 이러한 복잡한 과정을 피하고 핵심 개념을 위해 실제 리눅스 구조를 축약하여 의사 코드로 설명하도록 하겠습니다. 디스크 파일 입력을 위한 시스템 호출은 다음과 같은 작업을 수행합니다.


   1. 읽고자 하는 물리적 디스크 블록이 이미 커널의 시스템 버퍼에 있으면 이를 사용자에게 반환하고 사용자 모드로 복귀한다. 시스템 버퍼는 한 번 read 했던 디스크 블록에 대한 중복 디스크 입출력을 피하려고 커널이 커널 메모리 영역에 메모리 허용 한도 내에서 디스크 블록을 유지하고 있는 영역으로 리눅스에서는 전통적인 buffer cache나 가상 메모리 관리를 위한 page cache로 유지하는데 이는 파일 시스템의 종류에 따라 달라진다.(가장 많이 사용되는 ext3 file system의 경우 page cache를 사용한다.)


   2. 시스템 버퍼에 원하는 디스크 블록이 없거나 모자라는 경우에는 해당 블록을 시스템 버퍼에 적재하기 위해 버퍼를 할당받고, 디스크 입출력 정보를 탑재한 입출력 제어 블록을 생성하여 디스크 입출력 요구 큐에 등록한다.


   3. 디스크 입출력 요구 큐에 등록된 프로세스의 입출력이 완료될 때까지 프로세스는 대기 상태로 전환하며 문맥 교환을 통해 다른 프로세스에 CPU를 넘긴다. 즉, 프로세스는 시스템 호출 처리 루틴 내부에서 수행이 중지되는 상태가 된다.(Ready State)


   4. 입출력 큐에 등록된 입출력이 디바이스 드라이버에 의해 추후에 완료되면 프로세스는 준비 상태로 전이되고, 스케줄링을 받아 다시 실행이 속개되면(중지되었던 윛에서 다시 커널 모드 실행) 커널의 시스템 호출 처리 루틴은 시스템 버퍼의 내용을 사용자 영역에 복사하고 사용자 모드로 복귀하여 시스템 호출을 완료하게 된다.



  위 3번 과정에서의 수행이 중지된 상태(대기상태)에서 4번 과정의 상태로 오는 과정에는 디스크 디바이스 드라이버의 인터럽트 처리 루틴과 스케줄러(문맥 교환)가 작용하게 됩니다.

  즉, 3번에서 4번으로 되는 과정에는 디스크 입출력의 진행 및 완료, 준비 상태로의 전이, 커널 모드 실행모드로의 전이가 차례로 있어야 합니다.

  다음의 프로그램은 위와 같은 작업을 수행하는 커널 내부의 디스크 입력 루틴인 read와 디스크 인터럽트 핸들러를 의사 코드로 축약한 것입니다.






 1) sys_read: file offset 정보 획득

 2) vfs_read: open mode나 access check, security check

 3) do_sync_read (ext3 file의 경우, read 요청 관련 정보 생성)

 4) __generic_file_aio_read: 가상 메모리 관리의 page cache를 사용하는 모든 file system의 read routine

 5) do_generic_file_read -> do_generic_mapping_read






 * S/W 캐싱: 일반적인 디스크 기반의 운영체제에는 파일 입출력 및 가상 메모리 관리와 관련해서 많은 디스크 입출력이 발생합니다. 그러나 디스크 입출력은 CPU의 처리 속도보다 매우 느리므로 시스템의 성능 향상을 위해 디스크 입출력을 가능한 최소화해야 합니다. 이를 위해 대부분의 커널은 읽어온 디스크 블록의 재활용을 위해 커널 내의 일정 메모리 공간이 허용하는 한도 내에서 시스템 버퍼(각종 S/W 캐시나 가상메모리의 page cache)에 이를 저장합니다. 그리고 후에 발생하는 디스크 입출력 요구가 있을 때, 시스템 버퍼에 이 블록이 있으면 디스크 입출력없이 이를 찾아 재활용하게 됩니다.



 * DMA 디스크 입출력: 디스크 입출력은 블록(보통 4K ~ 32K 정도) 단위의 DMA(Direct Memory Access) 방식으로 진행됩니다. 이러한 디스크 입출력은 CPU의 처리 속도보다 상대적으로 느리므로 디스크 입출력 요구들의 큐가 형성되기 마련입니다. 디스크 입출력 요구 큐는 한 단위의 디스크 입출력에 필요한 모든 정보를 가지는 입출력 정보 블록의 리스트로 형성됩니다. 이러한 정보는


 <메모리 주소, 디스크 트랙/섹터 주소, 입출력 바이트 수, 읽기/쓰기>

 

 들로 구성되고 디스크 입출력은 CPU가 이러한 정보로 디스크 제어기에 입출력 명령을 내림으로써 시작됩니다. 일단 입출력이 시작되면 CPU는 더 이상 관여하지 않으며, 디스크 제어기가 메모리와 디스크 간에 자료를 직접 이동시킴으로써 입출력이 진행됩니다. 이 기간에 CPU는 다른 프로세스를 수행할 수 있으며, 디스크 제어기는 디스크 블록 입출력의 완료 시에 이 사실을 인터럽트를 발생시켜서 CPU에 통보합니다. 따라서 입출력 수행 중에 CPU는 명령어의 인출(fetch)을 위해, 디스크 제어기는 입출력 자료의 읽기나 쓰기를 위해 메모리에 동시에 접근하게 되고, 메모리에 대한 충돌 시에는 DMA가 우선권을 가집니다(cycle stealing).




  다음은 프로세스가 실질적인 대기 상태로 진입하는 block_wait_queue_running에 관한 설명입니다. 



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inline void block_wait_queue_running(request_queue_t *q) {
    prepare_to_wait_exclusive(.., &wait, TASK_UNINTERRUPTIBLE);
    /*
     * wait queue에 삽입, task 대기상태로 전환
     * 여기서 TASK_UNINTERRUPTIBLE 상태는 signal이 발생해도 깨어나지 않음
     */
    
    io_schedule();
    /*
     * 위 함수를 호출하여 문맥 교환(Context Switch)을 수행하고
     * 다시 깨어날 때에는 disk I/O는 완료되고 read 상태를 거쳐 scheduling을 받아 running 상태로 된 것임.
     * 이러한 과정에 관여하는 것은 disk 제어기에 대한 커널 interrupt handler
     */
 
}
 
cs




  위의 의사 코드에서 io_schedule() 함수는 현 실행 프로세스를 중지시키고 새로운 프로세스에 CPU를 할당하는 문맥 교환(Context switch) 함수입니다. 


  그 과정은 CPU 레지스터 내용과 같은 시스템 문맥을 중단될 프로세스의 태스크 구조체(PCB, task_struct)에 저장하고, 새로 선정된 플세스의 태스크 구조체 내에 보존되었던 문맥을 레지스터에 복귀시키는 것입니다. 저장 및 복귀되는 레지스터에는 프로그램 카운터(program counter)도 포함되기 때문에 이러한 문맥 교환이 일어나게 되면 자동으로 예전에 중지되었던 프로세스의 코드 부분으로 분기가 일어나 실행이 시작됩니다. 여기서 주의해야할 점은 문맥 교환으로 실행이 중지되는 프로세스도 이와 마찬가지로 언젠가는 다른 프로세스의 문맥 교환에 의해 현 지점으로 돌아오게 된다는 것입니다.

  

  따라서 switch_to는 함수로 표현되지만, 수행을 마치고 당장 복귀되는 일반 함수와 달리 다른 프로세스로 분기되었다가 다시 다른 프로세스의 문맥 교환에 의해 돌아오는 특수한 형태의 함수로 생각할 수 있습니다.





 3) 디스크 인터럽트 처리 루틴

  예제의 입출력 요청 프로세스가 커널 내부의 문맥 교환으로 중지된 상태에서 디스크 입출력의 진행은 디스크 제어기로부터의 인터럽트를 처리하는 커널 루틴이 담당하게 됩니다. 디스크 입출력 처리 루틴의 주요 작업은 크게 두 가지로 구성됩니다.


   1. 입출력 종료로 인터럽트가 발생하면 인터럽트 처리 루틴은 입출력이 종료된 프로세스의 태스크 구조체를 준비 상태(TASK_RUNNING)로 만들어 스케줄링 큐(run_queue)에 재삽입 한다. 이러한 행위를 일반적으로 wake_up 이라하며 준비 상태가 된 프로세스는 스케줄링을 기다리게 된다.


   2. 입출력 요구 큐에 등록된 다음 입출력 정보 블록으로 디스크 제어기에 입출력을 명령한다.





  위에서 입출력이 완료되어 다시 준비 상태(TASK_RUNNING)로 전환되는 프로세스는 일반적으로 사용자 모드에서 실행할 때보다 높은 우선순위를 할당받아 스케줄링 큐로 삽입됩니다. 그 이유는 시스템 호출 실행 중에 중지되었다가 다시 깨어난 프로세스를 일반 사용자 실행 모드의 프로세스보다 먼저 실행시켜 주기 위함입니다.






  일련의 인터럽트 처리 후에 준비 상태로 가게 된 예제 프로세스는 커널 내부의 여러 부분에 위치하는 문맥 교환 호출로 다른 프로세스로부터 CPU를 할당받아 자신이 예전에 문맥 교환을 한 지점으로 다시 복귀하게 됩니다. 커널 내부에서 문맥 교환이 일어나는 경우는 다음의 두 가지 유형으로 분류됩니다. 



   1. 프로세스가 시스템 호출을 하고 해당 커널 함수 안에서 입출력 요구 등을 하여 프로세스가 대기 상태로 전환하며 자발적으로 문맥 교환을 하는 경우


   2. 시스템 호출 및 인터럽트의 처리 과정에서 여러 가지 이유(타임 슬라이스의 소진, 프로세스의 대기 종료 및 생성, 우선순위의 변화 등)로 스케줄링이 필요하다는 플래그(current -> need_resched)가 설정된 경우, 인터럽트 처리 및 시스템 호출 완료 직후 사용자 모드로의 복귀 이전에 스케줄러가 수행되며 이때 우선순위가 현재 프로세스보다 높은 프로세스가 있으면 비자발적 문맥 교환이 일어난다. 현 프로세스의 타임 슬라이스가 다 소진된 경우에도 클럭 인터럽트 처리 과정에서 이러한 사실을 알게 되고, 이때에도 인터럽트 처리를 마치고 사용자 모드로 복귀하는 과정에서 문맥 교환이 발생한다.




 4) Timer 인터럽트 처리 루틴

  모든 운영체제는 일정한 시간 간격(1 milli-sec 또는 10 milli-sec)으로 인터럽트를 발생시키는 타이머를 갖습니다. 타이머 인터럽트는 정상정인 경우에 가장 우선순위가 높은 인터럽트입니다. 타이머 인터럽트가 발생하면 하던 작업(커널 작업이나 프로세스 실행)을 즉시 중지시키고 커널 내의 타이머 인터럽트 처리기(handler)로 분기하여 여러 가지 작업을 수행합니다. 수행되는 작업은 아래와 같습니다.


   1. 부팅 이후로부터의 시스템 시간 업데이트(jiffies ++)

   2. 주기적 커널 함수 작업 호출

   3. 현재 실행 중인 프로세스의 잔여 타임 슬라이스 감축, 잔여 타임 슬라이스가 0이 된 경우 다른 프로세스를 스케줄링하여야 하므로, 인터럽트 처리가 끝난 후 재 스케줄링 할 수 있도록 PCB 내의 need_resched 플래그를 1로 설정

   4. sleep 이나 alarm을 설정한 프로세스에 대한 서비스 수행

   5. 비사용 메모리가 부족할 경우 커널의 swapper 프로세스를 깨워(ready), swapper가 후에 스케줄링을 받으면 메모리를 비우는 작업을 수행토록 함

   6. return_from_interrupt 수행






 * Clock interrut handler

  Update the system time;

  Call timeout functions (every n ticks)

  Wakeup blocked processes in time queue; (processes called sleep etc.)

  Wakeup kernel processes if free memory size is under the threshold

  (wakeup Swapper kernel process and Swapout pages)

 

  current_PCB.time_slice_left--;

  if (time_slice_left == 0) currentPCB.need_resched = 1;

  Re-compute user mode priority; and do something;

  Return from Interrupt (== Ret from syscall)




타이머 인터럽트 처리







   Return_from_interrupt는 need_resched 플래그를 테스트하여 재 스케줄링이 필요하면 가장 높은 우선순위의 프로세스를 골라 현 프로세스와 문맥 교환을 수행합니다. 이와 같은 문맥 교환은 인터럽트 처리 내에서 수행되어서는 안 되고 반드시 인터럽트 처리가 종료된 후에 해야 합니다.

   리눅스의 인터럽트는 인터럽트를 disable 시키고 신속히 단기간에 처리하는 fast interrupt와 그 인터럽트 처리 과정이 길어 인터럽트 disable 모드에서 수행되는 인터럽트 처리(ISR: Interrupt Service Routine) 루틴과 인터럽트 enable 모드에서 시스템 호출 함수가 종료된 뒤에 수행되는 botom-half(또는 Soft IRQ)의 2단계로 처리되는 slow interrupt로 구분 됩니다. 

   키보드 인터럽트는 fast interrupt의 대표적인 예이고, 디스크나 타이머 인터럽트는 처리 과정이 길어 ISR과 bottom-half로 나누어 처리되는 slow interrupt 입니다. 



커널 내에서 문맥 교환이 일어나는 위치


System Call Functions 

Interrupt Handler 

  sys_call_rountine() {

     ..

    

     IO Request, lock waiting, etc.

     Voulantary Context Switch;

   

     ..

  

  }

  ret_from_syscall;

  

  if rescheduling is necessary:

     do Involuntary Context Switch; 

  irq_routine() {

      ..


      interrupt handling

      no context switch during in an interrupt handling!

      (because it is a jump)

 

      ..


  } 

  ret_from_syscal: (= return_from_interrupt)


  if rescheduling is necessary:

      do Involuntary Context Switch; 











 2.4 프로세스의 생성과 소멸

  리눅스 운영체제에서 프로세스의 생성 및 소멸 관련 기본 시스템 호출은 다음과 같습니다.






1
2
3
4
5
6
7
8
9
10
11
 
FORK(2)
#include <sys/types.h> man fork
#include <unistd.h>
 
pid_t fork(void);
 
    입력 값: 없음
    반환 값: - child 프로세스 : 0
            - parent 프로세스 : child 프로세스의 PID
            - 에러 : -1
cs




  fork 시스템 호출은 실행 중인 프로세스가 자신과 같은 프로그램 내용을 가진 새로운 프로세스를 생성할 때 사용합니다.


  fork를 호출한 프로세스를 parent 프로세스라 하고, 새로 생성되는 프로세스를 child 프로세스라고 합니다.


  parent 프로세스의 fork 호출이 완료되면 fork에서의 반환 값은 parent와 child 프로세스 두 곳에서 이루어집니다.


  parent에게는 child의 프로세스 식별자가 반환되고, child에게는 "0"이 반환되어 자신이 parent인지 child인지 구별되도록 합니다.


  새로 생성되는 child 프로세스의 data와 stack은 fork 시의 parent 상태를 복사하지만, 별도의 공간에 할당되어 각각 다른 영역을 사용합니다.






커널과 프로세스의 주소 공간











fork에 의한 프로세스 생성 이후의 주소 공간





 다음은 fork를 이용한 전형적인 협동 작업의 예입니다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
 
int global_int = 5;
 
 
int main(void) {
 
    pid_t pid;
    ...
 
    if ((pid = fork()) == 0) {    // child
        global_int = 6;
        printf(" child's data = %d\n", global_int);
        exit(0);
    } else {            // parent
        global_int = 7;
        printf(" parent's data = %d\n", global_int);
        wait();
    }
 
    printf(" parent's data = %d\n", global_int);
 
    return 0;
}
cs




   위의 예에서 fork 이후의 parent 와 child 프로세스는 커널에 의해 각각 다른 프로세스로 관리되는 것으로 각각 별도로 스케줄링을 받게 됩니다. 따라서 타임 슬라이스 만료나 입출력 요구에 의한 스케줄링으로 수시로 CPU가 각 프로세스에 할당되므로 두 프로세스의 실행 부분은 정해진 순서 없이 실행됩니다. 따라서 마치 두 프로세스가 동시에 수행되는 것처럼 보이고, 또한 실제로 다중 CPU 시스템에서는 병렬로 동시에 실행될 수도 있습니다.


   이러한 프로그램을 병행(Concurrent) 프로그램이라 합니다. 


   위 예에서 전역 변수인 global_int는 초기 값이 5 인 상태로 child 에 복사되지만, child는 별도의 데이터 공간을 갖기 때문에 fork 이후에 global_int는 실제로 두 개가 독립적으로 존재하게 됩니다. 프로세스 fork 이후에 각 프로세스의 메모리 영역은 프로그램 코드 부분인 TEXT 만 공유하고 나머지 부분은 별도의 독자적인 공간을 갖게 됩니다.




  2.4.1 프로세스의 종료와 child 프로세스 종료 대기

   위의 두 프로세스는 child 프로세스의 종료 / 소멸 시점을 기준으로 다시 하나의 수행 흐름으로 합쳐지는 동기화가 수행될 수 있는데, 다음은 이와 관련된 시스템 호출에 대한 설명입니다.


   프로세스의 종료를 위한 시스템 호출 exit은 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
 
 
_EXIT(2)
#include <stdlib.h>
 
void exit(int status);
 
    입력 값: 
        - status : 반환되는 상태 값
    반환 값: 없음
 
 
cs



   exit 은 프로세스의 종료와 함께 프로세스의 사용자 메모리 영역을 해제하며, wait 시스템 호출을 통해 child 프로세스의 종료를 기다리는 parent 프로세스를 깨워주는 역할을 합니다. 이때 exit의 입력 변수는 wait 시스템 호출의 매개 변수로 반환됩니다.


   parent 프로세스가 child 프로세스의 종료를 기다리는 동기화 도구로 사용하는 wait 시스템 호출은 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
WAIT(2)
 
#include <sys/types.h>
#include <sys/wait.h>
 
pid_t wait(int *stat_loc);
 
    입력 값: 
        - stat_loc : child 프로세스의 상태 정보를 받기 위한 버퍼
    반환 값:
        - 정상 : 하위 프로세스 식별자(PID)
        - 에러 : -1
 
cs




   이러한 wait 시스템 호출은 parent 프로세스가 병행으로 어떤 작업을 시킨 child 프로세스의 종료를 기다리는 경우에 사용합니다.


   wait와 exit의 병행으로 실행되는 두 프로세스에 의해 호출되므로 어떤 것이 먼저 일어나는지 알 수 없으며, wait 보다 exit 이 먼저 일어난 경우, wait에 의한 동기화 까지의 child의 상태를 zombie 상태라 하며, zombie 상태의 child는 parent의 wait에 의해 태스크 구조체가 시스템에서 완전 소멸하게 됩니다.

 




 2.4.2 child 프로세스의 상속

  fork 된 child 프로세스는 parent 프로세스의 변수 값들을 상속하고, 그 외에도 여러가지 상태나 사용 자원, 스케줄링 정보 등을 상속받게 됩니다.


  fork된 child 는 parent가 fork 이전에 open한 파일을 그 상태 그대로 상속하는데, open된 file descripter id와 각 파일의 내재적 접근 위치(read/write offset)도 상속하고 공유하게 됩니다.


  read/write offset을 공유하므로 parent와 child가 같은 파일에 입출력하게 되면 서로에게 영향을 미치게 됩니다. 아래 예를 보도록 합시다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
 
 
int main(void) {
    int old_fd, new_fd, n;
    char buf;
 
    old_fd = open("oldFIle"0);    // read-only mode
    new_fd = creat("newFile"0666);    // File creation
 
    fork();
 
    while ((n = read(old_fd, &buf, 1)) != 0)
        write (new_fd, &buf, n);
 
    close(old_fd);
    close(new_fd); 
 
cs





  위 프로그램은 새로 만든 newFile에 oldFile의 내용을 1 byte씩 모두 복사하는 프로그램 입니다.


  파일을 열고(open), creat 한 후, fork를 실행하므로 open된 파일의 상속에 의해 parent 와 child는 각각 두 파일을 사용할 수 있습니다.


  이때, 두 프로세스는 read / write offset을 고유하므로 파일의 크기는 두 배가 되지 않으며, 원래의 크기대로 복사를 수행하게 됩니다.


  여기서 주의할 점은 병행 프로그램의 특성상, parent가 read 가 실행하고, write 를 실행하기 이전에 스케줄링이 발생하여 child가 실행되고, child의 read와 write 후에 parent의 write가 실행될 수 있다는 점입니다.


  이런 시나리오로 병행 프로그램이 실행되면 복사된 파일의 크기는 같지만 출력 파일의 순서가 원래의 입력 파일의 순서에서 바뀔 수 있다는 점입니다.


  따라서 위와 같은 병행 프로그램은 파일의 복사 순서에 무관한 경우는 올바른 프로그램이지만, 순서까지 유지해야 하는 복사의 경우에는 잘못된 프로그램이 됩니다.


  이러한 문제를 임계 구역(Critical Section) 문제라 하는데, 이의 해결책으로 상호배재(Mutual Exclusion) 도구들이 운영체제에 의해 제공됩니다.






 2.5 프로세스의 프로그램 교체(로딩)를 위한 시스템 호출 exec 그룹

  실행 중인 프로세스가 exec 시스템 호출 그룹 중에 하나를 호출하게 되면 실행 중인 프로세스의 text, data, stack 영역 등은 지정된 실행 파일의 내용으로 적재되어 새로운 프로그램이 실행됩니다. 프로세스의 몸체는 새로운 실행 파일의 내용으로 교체되지만, 프로세스 id, parent-child 관계, open 하였던 파일 등 여러 프로세스의 환경은 그대로 유지됩니다.

  exec 시스템 호출 그룹은 execl, execle, execv, execve, execlp, execvp 등 인수가 약간씩 다른 여러 개의 호출로 구성됩니다. 이들 중 대표적인 것으로 execve를 살펴보도록 합시다.







1
2
3
4
5
6
7
8
9
10
11
12
13
EXECVE(2)
#include <unistd.h>
 
int execve(const char *path, const char *argv[], char const *envp[]);
 
    입력 값: 
        - path: 프로그램의 경로명
        - argv: 프로그램의 인수들에 대한 포인터
        - envp: 환경 변수로의 포인터
 
    반환 값:
        - 정상: 없음
        - 에러: -1
cs

  





  exec 계열의 시스템 호출은 새로운 프로그램을 적재(load)하고 적재된 프로그램의 main 함수를 실행하는 것이므로 정상적인 경우에는 return 되지 않습니다. 단, 지정한 실행 파일이 없거나 실행이 불가능한 경우에는 -1을 반환하게 됩니다. 



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
 * fork.c
 *
 *  Created on: 2015. 8. 12.
 *      Author: palpit
 */
 
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
 
#define PROMPT "$>"
#define LINE_LENGTH 80
 
int main(void) {
 
    char buffer[LINE_LENGTH];
    pid_t pid;
    int c, count = 0;
    int background = 0;
 
    for (;;) {
        printf(PROMPT);
        count = 0;
 
        while (1) {
            if ((c = getchar()) == EOF)
                exit(0);
 
            if (count < LINE_LENGTH)
                buffer[count++= c;
 
            if (c == '\n' && count < LINE_LENGTH) {
                buffer[count - 1= 0;
                break;
            }
 
            if (count >= LINE_LENGTH) {
                printf("input is Too Long !!\n");
                count = 0;
                printf(PROMPT);
                continue;
            }
        } // End of while
 
        if (strlen(buffer) == 0
                || (strlen(buffer) == 1 && buffer[strlen(buffer) - 1== '&'))
            continue// to for loop
 
        if (buffer[strlen(buffer) - 1== '&') {
            background = 1;
            buffer[strlen(buffer) - 1== '\0';
        }
 
        if ((pid = fork()) < 0) {
            perror("fork failed");
            exit(1);
        } else if (pid == 0) { // child process
            execlp(buffer, buffer, (char *0);
            perror("exclp failed");
            exit(1);
        }
 
        if (background == 1)
            continue;
 
        while (wait(&pid) != pid)
            ;    // wait for the exit of foreground
 
    } // end of for
 
    return 0;
}
 
cs






 위 프로그램은 간단한 쉘을 구성해 본 예제입니다. 위 프로그램을 수행되면 사용자 화면에는 "$>" 프롬프트(prompt)가 나타나고, 사용자는 자신이 실행시키고자 하는 프로그램을 입력하게 됩니다.


 parent 프로세스는 쉘과 같은 실행 형태로 child를 fork하고, fork 된 프로세스는 입력으로 받은 프로그램을 execlp를 통해 실행하게 됩니다.


 parent 프로세스는 fork와 exec를 통해 실행되는 child의 수행 완료를 waitpid를 통해 기다리고, child의 실행이 exit을 통해 완료되면 다시 프롬프트를 출력하여 다음 실행할 프로그램을 입력받습니다.


 단, 사용자의 프로그램 이름 입력시, "&" 기호를 붙이게 되면 이는 백그라운드 프로세스로 실행시키라는 의미로 parent는 child의 실행 완료를 기다리지 않고 바로 프롬프트를 출력하여 다음 프로그램 명을 입력 받습니다.


 백그라운드로 실행되는 프로세스가 언젠가 종료되면 지정된 포어그라운드(foreground)를 기다리는 wait loop에 의해 zombie 상태에서 소멸하게 됩니다.


다른 카테고리의 글 목록

CSE/Linux 카테고리의 포스트를 톺아봅니다
조회수 확인

 프로세스(Process)란 일반적으로 현재 실행 중인 프로그램을 말하며 태스크(task)라는 일반적인 용어로도 사용됩니다. 실행 중인 프로그램의 의미를 가지는 프로세스는 실행되는 동안 커널이 가진 자원(CPU, Memory, Devices, Files)을 독자적으로 차지해야 할 경우가 많습니다. 따라서 프로세스는 항상 커널의 자원을 차지하기 위해 경쟁하고 커널은 이들에게 효율적으로 자원을 스케줄링하여 할당하고 회수하는 역할을 합니다. 프로세스는 커널이 가진 여러가지 자원의 할당 및 사용을 위해 커널 함수를 호출해야 하는데, 이러한 커널 함수들을 일반적으로 시스템 호출(System call)이라 합니다. 따라서 동작 중인 컴퓨터 시스템은 커널과 프로세스 간의 관계에 의해 모든 것이 결정되는 것이라 볼 수 있습니다.

 

 프로세스나 스레드는 리눅스 커널 내부에서 모두 자원을 차지하기 위해 서로 경쟁하는 태스크들로 관리됩니다. 


 프로세스에는 커널이 시스템의 관리를 위해 생성한 시스템 프로세스와 사용자가 생성한 사용자 프로세스가 있으며 일반적으로 시스템 프로세스는 중요한 커널의 내부적 일을 담당하므로 스케줄링 우선순위가 높은 것이 보통입니다.


 사용자가 리눅스 시스템에 로그인하면 운영체제와 사용자의 대화를 위해 쉘 프로그램이 실행되는데, 쉘 또한 하나의 응용 프로세스이며 쉘을 통한 명령이나 사용자 프로그램의 실행 또한 모두 프로세스의 형태로 실행됩니다.









2.1 프로세스의 상태

 운영체제에 따라 상세한 프로세스의 상태 구분은 약간씩 달라질 수 있지만, 개념적으로 프로세스의 상태는 아래와 같이 분류됩니다.


 - 실행(Running) 상태: 프로세스에 CPU가 할당되어 실행 중인 상태.

 - 준비(Ready) 상태: 커널에 의해 스케줄링되어 CPU가 할당되면 실행될 수 있는 상태.

 - 대기(Blocked) 상태: 프로세스가 CPU 이외의 장치를 사용하는 입출력이나 메시지 수신등을 커널에 요구하게 되면 요구가 종료될 때 까지 프로세스는 CPU가 필요 없는 장치 상태가 되는데 이를 대기 상태라 함. 대기 상태동안 장치는 이 프로세스를 위해 동작하며, 동작 완료에 의한 입출력의 종료 또는 메시지  수신 등이 발생하면 해당 프로세스는 준비(Ready) 상태로 전이 됨. 커널은 한 프로세스의 대기 상태 동안 CPU를 다른 프로세스에 할당하여 CPU와 입출력 장치가 동시에 동작하도록 하여 자신의 효율성을 높이게 된다.









 2.1.1 프로세스의 상태 전이

  






  - CPU 할당을 기다리는 프로세스들은 스케줄링 큐(Scheduling Queue)에서 대기하다가 스케줄러에 의해 할당 받게 됨.

  - 커널은 CPU를 차지하여 실행 중인 프로세스의 CPU 독점을 방지하기 위해 Time slice에 의한 시분할 시스템의 개념을 도입하는 것이 보통.

  - Time slice란 프로세스의 CPU 독점을 방지하기 위해 매 CPU 차지시마다 CPU 사용의 한계 구간으로 주어지는 것으로 커널은 타임 슬라이스를 다 사용한 프로세스에서 일단 CPU를 회수하고, 다른 프로세스의 CPU 사용이 차례로 이루어진 후에 다시 CPU를 할당함.






 2.1.2 리눅스 프로세스의 상태

  1) 리눅스 프로세스의 실행(Running) 상태: TASK_RUNNING

   프로세스가 CPU를 회수 당하거나 반납하는 경우는 2 가지 형태가 있다.

     - 주어진 Time slice 소진 or 더욱 높은 우선순위에 의한 회수

     - 실행 중에 입출력(I/O)이나 동기화(이벤트 대기)에 관계된 시스템 호출을 하여 CPU를 커널 내부에 스스로 반납하는 경우


   운영체제에 따라 차이점이 있지만 리눅스의 경우에는 일반적으로 프로세스의 실행상태는 커널 모드 실행 상태와 사용자 모드 실행 상태의 두 가지로 구분됩니다.




실행 모드 구분 

실행 코드 

시스템 보호 측면 

사용자 모드

(User Mode)

 사용자 프로그램의 코드가 실행된다.

 커널함수 호출이 아닌 일반 라이브러리 함수의 실행도 이에 포함된다. 

 실행중인 프로세스가 다른 프로세스나 커널의 메모리 영역을 침범하지 못하도록 메모리 보호 하드웨어가 작동된다. 

커널 모드

(Kernel Mode)

 프로세스가 실행 중인 메모리에 상주하는 커널 함수를 호출하였거나 하드웨어 인터럽트가 발생하여 커널 안의 코드가 수행될 때 

 사용자 모드에서는 불가능한 입출력이나 인터럽트 및 시스템 제어에 관련된 특수 명령어들을 수행할 수 있고, 커널 안의 코드를 수행하면서 다른 프로세스의 영역에도 접근할 필요가 있기에 메모리 보호 하드웨어도 통과 할 수 있다. 



   위에서 커널 모드실행은 프로세스가 커널의 시스템 호출을 함으로써 이루어진 다고 설명하였습니다. 그러면 시스템 호출은 무엇일까요? 아래 설명을 보도록 하겠습니다.



 *시스템 호출(System Call)


설명 보기



 










  

  2) 리눅스 프로세스의 준비(Ready) 상태: TASK_RUNNING

   리눅스의 경우 프로세스에 관한 커널의 모든 정보는 태스크 구조체(task_struct)에 저장되는데, 실행상태와 준비상태의 프로세스의 태스크 구조체들은 모두 스케줄링 큐에 연결 리스트 형태로 저장됩니다.





  3) 프로세스의 대기(Blocked) 상태: TASK_INTERRUPTIBLE 또는 TASK_UNINTERRUPTIBLE

   프로세스 상태가 대기 중일때, 그 태스크 구조체는 스케줄링 큐에서 제거되어 특정 대기큐에 소속되는 것이 보통입니다. 입출력이 완료 되거나 기다리던 이벤트가 발생하면 대기 상태의 프로세스는 다시 준비(Ready) 상태가 되어 그 태스크 구조체를 스케줄링 큐로 복귀됩니다.

   리눅스 운영체제의 경우, 대기 상태는 실질적으로 두 가지로 구분되는데, 첫째는 TASK_INTERRUPTIBLE 상태이고 다른 하나는 TASK_UNINTERRUPTIBLE 상태입니다.

   

    - TASK_UNINTERRUPTIBLE: 대기 중에 해당 프로세스에 응급 이벤트의 발생을 알리는 신호가 전달되어도 원래의 기다리는 이벤트가 발생할 때 까지 대기를 유지하는 상태

    - TASK_INTERRUPTIBLE: 신호에 의한 응급 처리를 위해 기다리던 이벤트에 대한 대기를 중지하고 준비 상태로 복귀하는 경우





  4) 프레서스의 좀비(EXIT_ZOMBIE) 상태

   프로세스의 수행이 종료되면 모든 포르세스의 task 구조체와 메모리 영역의 프로그램들은 제거되어야 합니다. 그러나 리눅스 커널에서는 해당 프로세스를 생성한 후, 생성 프로세스의 종료를 기다리는 부모 프로세스에게 종료 프로세스에 대한 정보를 전달해야 하므로, 정보 전달 시까지 task 구조체를 유지합니다. 이렇게 종료는 되었지만 task 구조체는 유지하고 있는 상태를 좀비 상태라고 합니다.








  5) 프로세스의 중지(TASK_STOPPED) 상태

   디버깅 목적을 위해 프로세스가 일시 중지된 상태입니다. 시그널이나 디버깅 개시 명령으로 재개합니다.




  6) 기타

   그 이외에도 부모 프로세스가 없는 경우의 EXIT_DEAD, 태스크 삭제 직전의 TASK_DEAD 등의 상태가 존재합니다.








2.2 프로세스의 문맥 교환(Context switch)

 2.2.1 프로세스의 문맥(context)과 태스크 구조체

  정적자원인 프로그램과 달리 프로세스는 항상 실행되며 상태 변화를 계속하는 동적인 개체입니다. 또한, 프로세스는 수행 중에 자원에 대한 할당 대기와 외부 인터럽트 처리와 같은 작업에 의해 언제든지 수행이 중지되고, 그 후에 다시 속개되는 일이 반복됩니다.

  따라서 프로세스의 중지 시점에는 프로세스의 실행에 필요한 모든 정보와 환경들이 저장되어야 하고, 속개시에는 중단 시점의 내용이 그대로 복원되어야 합니다. 이렇게 중지시에 저장되고 속개시에 복원되는 프로세스의 실행에 필요한 모든 정보를 프로세스의 문맥(context)이라 합니다.


  다음은 프로세스 문맥의 구성입니다.


설명 보기





  위 문맥구성에서 프로세스 공간 영역은 메모리에 할당되므로 프로세스의 사용자 공간이 유지되면 자동으로 보존됩니다. 가상 메모리 관리에 의해 메모리에서 퇴출 당하는 영역이 발생하나 필요시에는 다시 메모리로 복귀합니다. 프로세스와 관련된 모든 문맥은 PCB에 보관되는데, 반드시 보존되고 실행 개시 시점에 복원되어야 하는 것은 대부분 CPU 레지스터의 내용입니다. 즉, 레지스터의 내용은 CPU를 차지하는 프로세스가 실행되는 동안 모두 변화되기 때문에 프로세스 중지시에는 반드시 보존해 두었다가 수행 속개시 다시 복원되어야 합니다.






스케줄링 큐










 2.2.2 프로세스 간의 문맥 교환(Context switch)

  스케줄링으로 실행 중인 한 프로세스에서 다른 프로세스로 CPU가 할당될 때, 중단되는 프로세스가 사용중인 모든 레지스터의 내용은 보존되고, CPU가 서로 할당되는 프로세스는 자신이 이전에 중단되었던 시점에서 보존된 모든 레지스터의 내용이 복원되어야 합니다.

  따라서 CPU 내의 레지스터 입장에서는 현 내용이 보존되고 새로운 내용이 로딩되므로 이를 Context Switch 라고 합니다.

 

  교최되는 레지스터에는 프로그램 카운터(PC) 레지스터도 포함되므로, 실제로 문맥교환은 레지스터 내용 교체 후에 새로운 프로세스로 분기(Jump)하는 것이 됩니다. 리눅스 커널 내부의 스케줄러에 의해 호출되는 context_switch() 함수는 메모리 관련 정보에 대한 문맥 교환을 하고, switch_to() 함수를 호출하여 레지스터의 내용에 대한 문맥 교환을 하게 됩니다. 즉, switch_to() 함수에서 새로운 프로세스로 분기하는데, 유의할 점은 switch_to() 함수에 의해 중단되는 프로세스는 언젠가는 다시 다른 문맥 교환으로 다시 이 지점에서 수행이 속개된다는 점입니다. 









다른 카테고리의 글 목록

CSE/Linux 카테고리의 포스트를 톺아봅니다
조회수 확인

1.1 리눅스의 사용자 계정

 리눅스(Linux)는 다중 사용자를 위한 운영체제입니다. 여러 사용자가 사용하는 시스템은 비적절한 접근으로부터 시스템을 보호해야 합니다.

 

 따라서 사용자는 사용자 식별자(username or id)패스워드 입력에 의한 접근 허가를 받아야 합니다. 


 사용자의 이름과 패스워드는 /etc/passwd 파일에 수록되어 있습니다. 패스워드의 경우는 암호화되어 저장되어 집니다.


 접속한 사용자의 id는 사용자가 실행시키는 여러 프로그램(프로세스)들에 대해 시스템의 자우너 사용 때의 권한을 결정하는 요소가 됩니다.


 일반 사용자 이외에 시스템의 관리를 위해 시스템 자원에 모두 접근할 수 있는 특수 사용자(super user)가 있는데, 이는 root라는 이름으로 로그인하게 됩니다.


 사용자들은 어떤 자원에 대해 같은 권한을 갖게 하려고 그룹에 소속될 수도 있습니다. 따라서 사용자는 로그인 후 그룹 식별자(id)도 갖게 되는데 사용자의 그룹 id는 /etc/group에 저장됩니다.








1.2 리눅스의 부팅 과정

 일반적으로 운영체제는 항상 메모리에 상주하는 핵심 부분인 커널(kernel)과 필요 시에만 메모리로 로딩되는 여러 가지 형태의 모듈, 커널 프로세스 및 기본적 응용 서비스 프로그램들로 구성됩니다.


 부팅은 운영체제의 핵심인 커널을 메모리로 로딩하는 과정을 말합니다.


 부팅과정은 하드웨어 시스템의 종류에 따라 방법이 여러가지입니다. 그 중에서 가장 많이 사용되는 x86 계열의 CPU로 예를 들어보겠습니다.


 시스템이 전원이 켜지면 ROM에 저장된 BIOS(Basic Input/Output Service) 프로그램이 실행되어, 시스템의 메모리입출력 장치 등에 대한 기본적인 테스트와 초기화가 이루어 집니다.


 그 이후 BIOS는 지정된 부팅 매체의 정해진 장소(MASTER 부트 레코드로 디스크의 경우 첫 번째 트랙/섹터)에서 커널을 메모리로 적재하는 일을 하는 부트스트랩 로더 프로그램(Bootstrap Loader)을 메모리로 읽어 들입니다.


 이러한 부트 로더는 대표적인 것들로 LILO(LInux LOader), GRUB(GNU GRand Unified Bootloader), RedBoot 등 여러 가지가 있습니다.


 부트 로더가 실행되면 사용자가 원하는 커널을 적재할 수 있습니다. 일반적으로는 /boot 디렉터리의 vmlinuz 커널 이미지 파일이 로드 됩니다.


 vmlinuz는 리눅스 커널의 압축 이미지입니다.


 사용자가 새로 구성한 커널이 있을 때는 대화식으로 그 파일 이름을 주게 됩니다.


 커널 이미지가 메모리로 적재되면 다음과 같은 초기화 과정을 거쳐 시스템을 실행상태로 만듭니다.


  1) 커널 기본 자료 구조, 클럭, 입출력 장치 등 초기화

  2) 가상 메모리 시스템(Page Mapping) 초기화

  3) 최초의 커널 프로세스로서 가상 메모리 관리를 위한 스와퍼(Swapper) 프로세스 생성

  4) 프로세스 식별자가 1이며 최초의 사용자 프로세스인 init 프로세스가 생성, 실행. init 프로세스는 이후의 모든 사용자 프로세스의 선조가 되는 프로세스로, 프로세스를 생성하고 동작을 감시하기 때문에 절대 끝나지 않음.






1.3 GNU gcc 컴파일러

 GCC는 GNU의 C, C++, Objective-C, FORTRAN, Java, Ada 언어를 위한 컴파일러들과 각 언어를 위한 libstdc++, libgcc 등의 라이브러리 모음으로 오픈 소스 소프트웨어 입니다.

 GCC는 C와 C++에 대해 ANSI C 표준, C99 표준 등을 선택사항으로 제공하고 그 이외에도 여러 형태의 확장된 기능을 제공합니다. 또한, GCC는 사용하는 CPU를 위한 코드뿐만 아니라 다른 타겟 CPU를 위한 코드 생성 즉, 크로스 컴파일(cross compile) 기능도 제공하여, 여러 환경에서의 개발에 필수적으로 사용되는 대표적인 유닉스와 리눅스에서의 개발 도구라 할 수 있습니다.

 GCC의 실행 단계는 매크로 등을 처리하는 전처리(preprocessing), 문법 체크와 중간 코드를 생성하는 front-end 처리, 타겟 CPU를 위한 기계어 코드를 생성하는 back-end 처리, 여러 개의 오브젝트 프로그램을 하나의 실행 파일로 결합 생성하는 링킹(Linking) 단계 등으로 나누어 진행됩니다.


 일반적으로 C 프로그램을 위해서는 gcc, C++ 프로그램을 위해서는 g++를 명령어로 입력합니다.




 1) 소스 파일의 컴파일

   현 디렉터리에 있는 어떤 프로그램 exam1.c 를 컴파일하여 실행 파일을 생성하려면 다음의 쉘 명령을 수행시키면 됩니다.





    $ gcc exam1.c





   이 명령어가 성공적으로 수행되면 현 디렉터리에 "a.out" 이라는 이름의 실행 파일이 생성됩니다. 이러한 a.out을 실행하려면 다음의 쉘 명령을 입력합니다.




    $ ./a.out

   



   이러한 gcc 나 g++는 수 많은 종류의 컴파일 옵션을 제공하는데 이에 대한 상세한 사항은 http://gcc.gnu.org/ 를 참조하기 바라며, 이번 장에서는 중요한 몇 가지의 옵션만 설명하도록 하겠습니다. 

   



 2) 출력 제어 옵션

   gcc의 실행 결과로 생성되는 출력에 대한 제어 옵션들로 구성됩니다.



   -c
     입력 파일을 컴파일이나 어셈블(assemble)만 하여 확장자 .o의 파일로 생성합니다. 파일들에 대한 링크를 수행하지 않아 실행 파일이 생성되지 않습니다.

  
   -S
     기계어 코드 생성 이전의 어셈블리어 파일 생성 단계까지 수행합니다. 확장자를 .s로 하는 어셈블리어 파일이 생성됩니다.


   -o file
     a.out 대신에 주어진 이름의 실행 파일을 생성합니다.

   -v
     컴파일의 각 단계에서 실행되는 명령들과 gcc의 버전 번호를 출력합니다.





 3) 디버깅 옵션

   -g
     운영체제에 정해진 형태의 실행 프로그램 디버깅 정보를 생성하여 프로그램 실행 단계에서 GDB 디버거가 동작할 수 있도록 합니다.




 4) 최적화 관련 옵션

   -O (or -O1)
     컴파일 시간이 그리 많이 걸리지 않는 정도에서 프로그램의 실행 시간과 공간을 줄이기 위한 최적화 컴파일을 수행합니다. 이러한 최적화는 디버깅 단계에서는 굳이 사용할 필요가 없고, 프로그램의 테스팅이 종료된 뒤에 사용하는 것이 좋습니다.

   -O2 and O3
     O 뒤의 숫자가 클 수록 최적화를 위해 많은 요소를 반영하게 됩니다.

   -Os
     프로그램의 크기를 최적화 합니다.


   


 5) 전처리 옵션

   -D name
     name 매크로의 값을 1로 정의합니다.

   -D name=definition
     name 매크로의 값을 definition으로 정의합니다.

   -i dirname
     헤더 파일을 찾는 시스템의 정해진 디렉터리에 dirname을 추가합니다.

 


 6) 링킹 옵션

   -llibrary
     실행 파일 링킹 시에 지정된 라이브러리에서 필요 함수들을 찾아 링킹합니다. 지정된 라이브러리는 /usr/lib와 같은 시트메 라이브러리 및 사용자에 의해 지정된 디렉터리에서 차례대로 찾습니다. 예를 들어 sin 같은 함수를 사용할 경우에는 수학 함수 라이브러리인 libm.a에서 해당 함수를 가져오기 위해 다음과 같은 쉘 명령을 사용합니다.


   $ gcc exam1.c -lm










 

   -Ldirname
     라이브러리를 검색하는 디렉터리 목록에 dirname을 추가합니다. 

   -s
     실행 파일에서 모든 심볼 테이블 정보와 재배치(relocation) 정보를 제거합니다.







1.4 쉘(Shell)
 시스템에 로그인하게 되면 제일 처음 접하는 게 쉘 입니다. 

 쉘은 입력하는 모든 명령을 해석하고 실행합니다. 현재 시스템에서 사용할 수 있는 쉘은 /etc/shells에 등록되어 있습니다.

 특정 쉘을 사용하고 싶다면 쉘의 설치 후 /etc/shells 에 등록해 주어야 합니다.

 사용자 각자의 쉘은 /etc/passwd 파일의 username 필드 뒤쪽에 등록되어 있습니다.



  1) 리눅스에서 사용 가능한 쉘의 종류
   Bash
     Bourne Again 쉘. 리눅스에서 가장 많이 사용하고 있습니다. POSIX 호환이며 GNU 프로젝트에 의해 만들어지고 배포되고 있습니다.

   csh
     C 쉘. 버클리에서 개발되었습니다.

   ksh
     콘(Korn) 쉘. 유닉스에서 가장 많이 사용되는 쉘입니다. csh로부터 많은 기능을 도입했습니다.

   sh
     Bourne 쉘. 오리지날 쉘.

   tcsh
     확장 C 쉘.

   Zsh
     Z 쉘. 가장 최근에 나온 쉘입니다.



  2) 현재 사용중인 쉘의 확인



   $ echo $SHELL



 


    대부분 리눅스에서는 Bourne Again 쉘인 bash를 사용하고 있을 것입니다. BSD나 다른 계열의 리눅스를 사용하고 있다면 bash가 아닐 수도 있습니다.




  3) 쉘 바꾸기



   $ chsh

   Changing shell for userID.

   Password:

   New shell [/bin/csh]: /bin/bash

   Shell changed.

   $










1.5 환경정정 설정
 X 윈도우를 설정하는 과정은 크게 두 가지로 나누어집니다. 하나는 하드웨어 설정하는 부분이고 다른 하나는 일반적인 환경 설정입니다.


 1) 하드웨어 설정
  최근 X 윈도우 설정을 위한 하드웨어 설치는 상당히 간단해졌습니다. 

  리눅스가 지원하는 하드웨어 검색 프로그램으로 쉽게 설치할 수 있게 되어 있기 때문입니다. 

  그러나 검색 프로그램에서 찾지 못한 하드웨어는 하나하나 설정해 주어야 하는데 그래픽 카드와 모니터가 그 예가 될 수도 있습니다.


  $ Xconfigurator 를 통해서 X 윈도우에 대해 설정을 할 수 있습니다. 



 2) 환경 설정
22222  배포판에 따라서 윈도우 매니저가 다르지만, 일반적으로 많이 사용되고 있는 것은 Gnome이나 KDE 일 것입니다. 로그인 창에서 윈도우 매니저를 설정하고 로그인 할 수 있으며 로그인 후 윈도우 매니저의 세부 항목도 개인 취향에 맞도록 수정할 수 있습니다.






1.6 계층 구조적 파일 시스템

 /bin

  11111일반적인 리눅스 명령어 수행을 위한 실행 파일이 이 디렉터리에 있으며, login, shell 등의 파일도 여기에 있습니다.



 /boot

  LILO 나 GRUB과 같은 부트 로더가 부팅 시 이 디렉터리에 있는 커널 이미지를 사용합니다. 커널 이미지 및 System.map 과 같은 파일이 위치합니다.



 /dev

  이름 그대로 디바이스 노드가 위치합니다. 

  

  이 디렉터리의 파일은 메이저와 마이너 넘버로 각 장치를 구분하며, mknod 명령을 노드를 만들 수 있습니다.


  리눅스는 입출력 장치를 일반적인 파일과 같이 취급할 수 있도록 이 디렉터리를 통해 Devfs(디바이스 파일 시스템)을 제공합니다. 


  Devfs에 대한 상세한 사항은 커널 소스 내의 Documentation//filesystems/devfs 을 참고하시기 바랍니다.




 /etc

  여러 설정파일들이 위치하며 sysinit 스크립트 및 마운트(mount) 할 디스크 파티션 및 파일 시스템을 포함하고 있는 fstab 등이 위치하는 필수적인 디렉터리입니다.




 /lib

  시스템에 있어서 가장 중요한 glibc(c 라이브러리)를 포함하여 모든 라이브러리 파일이 위치하는 곳 입니다. 어떠한 리눅스 명령어도 glibc가 없다면 실행될 수 없습니다.




 /proc

  /proc은 가상 파일 시스템으로 /proc 내의 파일은 커널이 만들어 줍니다. 


  이 디렉터리는 CPU, 프로세스, 메모리, 디바이스, 인터럽트 등과 관련된 여러 가지 유용한 정보를 파일 형태로 가지고 있습니다.


  


 /sbin

  /bin 디렉터리와 마찬가지로 리눅스 명령어가 위치하지만, 이 디렉터리에 위치한 명령어는 시스템 관리와 관련된 것들 입니다. 따라서 일반 사용자의 실행 PATH 경로에는 기본적으로 포함되지 않습니다. init, reboot 등의 명령어들이 여기에 위치합니다.




 /tmp

  이 디렉터리는 각종 작업에서 사용되는 임시 파일들을 위한 디렉터리입니다.




 /usr

  사용자를 위한 공간으로 일반적으로 새로운 프로그램 설치 시에 사용하는 공간입니다.




 /var

  시스템과 관련된 로그파일, 메일이나 lock 파일 등이 위치하는 곳입니다.







다른 카테고리의 글 목록

CSE/Linux 카테고리의 포스트를 톺아봅니다