1. perror / strerror

 

1) perror

<stdio.h>
void perror(const char *s);

- 입력받은 문자열과 errno에 대한 오류 메시지 결합하여 출력

- 오류가 발생하면 해당 오류 코드가 errno에 저장되는데 이를 가져와서 출력하는 것임

2) strerror

<string.h>
char *strerror(int errnum);

- 오류 코드를 매개변수로 받아 해당 오류코드에 대한 오류 메시지를 반환

- 문자열을 반환하므로  printf 함수와 함께 사용해야 함

2. access

#include <unistd.h>
int access(const char *path, int mod);

- 파일이나 디렉토리에 대한 접근 권한을 확인하는 데 사용

- 접근 권한 확인 후 성공하면 0, 실패하면 -1을 반환

- F_OK : 파일의 존재 여부 확인 / R_OK : 읽기 권한 확인 / W_OK : 쓰기 권한 확인 / X_OK : 실행 권한 확인

 

3. dup / dup2

int dup(int oldfd);

- oldfd로 지정된 파일 디스크립터를 복제하여 새로운 파일 디스크립터를 생성

- 성공시 새로운 파일 디스크립터를 반환하며, 실패시 -1을 반환

#include <unistd.h>
int dup2(int oldfd, int newfd);

- oldfd로 지정된 파일 디스크립터를 newfd로 지정된 파일 디스크립터로 복제

- newfd가 이미 열려 있는 경우, new_fd를 닫고 oldfd를 복제하여 newfd로 사용

- 위 경우 표준 출력의 내용이 output.txt에 기록

 

4. execve

#include <unistd.h>
int execve(const char * pathname, char *const argv[], char *const envp[]);

- 지정된 경로 (pathname)의 프로그램 파일을 실행하고, 실행 중인 프로세스의 이미지를 해당 프로그램으로 교체 한 뒤 argv와 envp의 내용을 새로운 프로그램에 전달

5. fork

#include <unistd.h>
pid_t fork(void);

- 기존 프로세스에서 새로운 자식 프로세스를 생성 (각 프로세스는 동일한 코드와 상태를 가지지만, 서로 다른 프로세스 ID(PID)를 가짐

- 자식 프로세스인 경우 0을 반환하며, 부모 프로세스인 경우 현재 프로세스가 복제되어 자식 프로세스가 생성

6. pipe

- 파이프는 두 개의 파일 디스크립터로 이루어진 단방향 통신 채널로, 한 쪽에서 쓰여진 데이터를 다른 쪽에서 읽을 수 있게 해 줌

#include <unistd.h>
int pipe(int pipefd[2]);

-pipe() 함수는 프로세스 간 통신을 위해 사용되며, 파이프를 통해 데이터를 안전하게 전송

- pipefd[0]은 읽기용 파일 디스크립터를 저장,  pipefd[1]은 쓰기용 파일 디스크립터를 저장

-pipe() 함수를 호출하면 파이프가 생성되고, 연결된 파일 디스크립터를 pipefd 배열에 저장

- 파이프는 프로세스간 통신 방법 중 하나로, 파이프는 읽기를 위한 파일 디스크립터와 쓰기를 위한 파일 디스크립터, 총 2개로 이루어짐

- 한 프로세스가 파이프의 쓰기용 파일 디스크립터를 통해 데이터를 파이프에 쓰면 다른 프로세스가 읽기용 파일 디스크립터를 통해 읽음

- fork() 함수가 호출되면 부모프로세스와 거의 동일한 자식 프로세스가 생성.

- fork()를 호출한 후에는 두 프로세스(부모와 자식)이 있으므로, 한 프로세스에서는 else if 블록이 실행되고, 다르 프로세스에서는 else 블록이 실행]

- 따라서 위의 코드를 실행하면 부모 프로세스가 "Hello, child!"라는 메시지를 자식 프로세스로 전달하고, 자식 프로세스는 해당 메시지를 받아 출력

 

7. unlink

- 파일 시스템에서 파일을 삭제하는데 사용되는 함수로, 해당 파이르이 디렉토리 엔트리를 제거하여 파일을 삭제

- 삭제 성공하면 0, 실패하면 -1.

#include <unistd.h>
int unlink(const char *pathname);

- 상대경로, 절대경로 둘 다 사용할 수 있음

 

8. wait / waitpid

1) wait

-부모 프로세스가 자식 프로세스가 종료될 때까지 기다리도록 하는 역할

#include <sys/types.h>
pid_t wait(int *status);

- status 매개변수를 통해 어떤 방식으로 자식 프로세스가 종료 되었는지를 부모 프로세스가 알 수 있음

- 종료한 자식 프로세스의 프로세스 pid를 반환

 

* 넘겨주었던 status를 아래와 같은 매크로에 넣어 종료 상태를 확인할 수 있음.

WIFEXITED(status) 자식 프로세스가 정상적으로 종료되었는지 확인. 정상 종료되었다면 이 매크로는 참 값을 반환
WEXITSTATUS(status) 자식 프로세스의 반환 값을 얻기 위해 사용. WIFEXITED(status)가 참일 때만 호출
WIFSIGNALED(status) 자식 프로세스가 시그널에 의해 종료되었는지 확인하는데 사용
WTERMSIG(status) 자식 프로세스를 종료시킨 시그널의 번호를 반환. WIFSIGNALED(status)가 참일때만 호출
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // This is the child process.
        printf("I'm the child process.\n");
        exit(7);
    } else {
        // This is the parent process.
        printf("I'm the parent process.\n");
        
        int status;
        pid_t child_pid = wait(&status);
        
        if (WIFEXITED(status)) {
            printf("Child %d ended normally. Exit status: %d\n", child_pid, WEXITSTATUS(status));
        } else {
            printf("Child %d ended abnormally.\n", child_pid);
        }
    }
    
    return 0;
}

 

2) waitpid

- 자식 프로세스의 상태 변화를 기다리는데 사용되며, wait() 함수보다 더 풍부한 기능을 제공

pid_t waitpid(pid_t pid, int *status, int options);
pid 대기하려는
자식 프로세스의 ID를
지정
pid > 0 프로세스 ID가 pid인 자식 프로세스를 기다림
pid = 0 호출자와 같은 프로세스 그룹 ID를 가진 모든 자식 프로세스를 기다림
pid = -1 모든 자식 프로세스를 기다림
pid < -1 프로세스 그룹 ID가 pid인 절대 값인 자식 프로세스를 기다림
status 자식 프로세스의 종료 상태를 저장하는 포인터
options 옵션을 설정하는 플래그 WNOHANG 상태 변경을 감지할 수 있는 자식 프로세스가 존재하지 않을 경우 즉시 0을 반환
WUNTRACED 자식 프로세스가 중지된 경우에도 (일시 정지와 같은 상황) waitpid()를 반환하여 부모 프로세스가 자식 프로세스의 일시 정지 상태를 알 수 있음

 

1.  가상머신이란?

1) 정의 및 작동 방식

가상머신 : 물리적 컴퓨터와 동일한 기능을 제공하는 소프트웨어 컴퓨터

가상화의 발전과정

- 기본적으로 컴퓨터는 하드웨어 위에 운영체제가 올라가있고, 그 위에서 응용 프로그램이 작동하는 방식으로 동작. 이 때 메모리에 상주하는 운영체제의 핵심 부분을 커널이라고 한다.

- 가상머신은 하나의 물리적인 컴퓨터에서 여러개의 독립적인 가상환경을 생성하여 동작시키는 기술

-  하이퍼바이저는 가상머신의 생성, 자원 할당, 네트워킹, 저장장치 관리, 보안 등을 처리하며, 가상머신들 간의 리소스 분할과 격리를 관리하여 각각의 가상머신이 독립적으로 동작 할 수 있도록 한다.

- 도커는 컨테이너 기술을 사용하여 호스트 시스템의 운영체제를 공유하기 때문에 더 가볍고 효율적인 리소스 사용이 가능

 

2) 목적

- 하나의 컴퓨터로 동시에 서로 다른 2개 이상의 운영체제를 실행하고 싶을 때

- 하나의 컴퓨터의 자원을 여러 명에게 나누어주고자 하는데 각 사용자 간 상호 간섭을 없애고 싶을 때

- 컴퓨터의 다른 영역에 영향을 주지 않는 독립적인 환경을 만들고 싶을 때

- 각 가상머신은 다른 가상머신과 격리되어 있으며, 서로의 운영체제와 애플리케이션에 영향을 주지 않는다. 또한 물리적인 하드웨어와 독립적이므로 다른 호스트 컴퓨터로 이동하거나 클라우드 환경에서 실행될 수 있다.

 

2. 파티션과 LVM(Logical Volume Manager)

파티션 : 물리적인 디스크를 여러 논리적인 섹션으로 분할하는 것. 일반적으로 운영체제, 응용 프로그램, 데이터 등을 구별하기 위해 사용

LVM : 물리적인 디스크를 논리적인 단위로 관리하는 프레임워크. 논리적인 디스크 공간을 필요에 따라 동적으로 변경하거나 재분배 할 수 있다.

파티션이 디스크를 분할하는데 초점을 맞춘 기법이라면, LVM은 디스크를 분할하고 합치고 크기를 동적으로 조정하는 등 더욱 유연한 디스크 관리를 가능하게 한다.

 

가상머신에서 파티션 분할

리눅스 시스템에서는 하나의 하드디스크에 최대 4개의 기본 파티션을 생성할 수 있으며, 필요에 따라 더 많은 파티션을 생성하고 싶을 때 확장 파티션을 생성한다. 즉, 4개 이상의 파티션이 필요하면 하나의 파티션을 확장 파티션으로 잡고 확장 파티션 내에서 다시 논리 파티션을 구분하는 방식으로 우회하는 것이다. 

위 사진은 가상머신에서 리눅스 파일 시스템 계층 구조를 간략하게 구현한 것이다.

/ 루트 디렉토리로, 시스템이 시작되는 첫 번째 위치
/boot 리눅스 시스템을 부팅하는데 필요한 파일들이 저장되어 있는 디렉토리
[SWAP] 컴퓨터의  RAM이 가득 찼을 때, 리눅스 시스템이 임시로 사용하는 디스크 공간
/home 일반 사용자들의 홈 디렉토리를 담고 있다. 사용자를 등록하면 일반적으로 /home/계정명으로 사용자 계정 디렉토리가 생성되고 운영된다.
/var 시스템이 실행되면서 내용이 자주 변경될 수 있는 데이터를 저장 (로그 - 시스템 상태, 동작 등을 기록 / 스풀 - 프린터 작업, 메일, 스케줄 작업 등)
/srv 시스템에서 제공하는 서비스 데이터를 저장
/tmp 시스템이 재부팅 될 때 삭제되는 일시적인 파일들을 저장

 

3.Sudo

- 'super user do'의 약자로, 각종 명령어 앞에 sudo를 붙이면 최고 관리자 권한으로 실행된다.

- etc/sudoer 설정 파일에 명시된 사용자만 사용 가능하다.

비밀번호 정책설정

- Deafult env_reset : sudo를 사용할 때 사용자가 시스템의 환경변수만 사용되게 하도록 제한. 환경변수가 예기치 않게 변경되어 의도하지 않은 동작을 유발 하지 않도록 함.

- mail_badpass : 비밀번호를 잘못 입력한 경우, 이 설정에 따라 알림 이메일이 관리자에게 전송.

- secure_path : sudo를 사용하여 실행할 수 있는 명령어의 경로를 제한. 이를 통해 sudo를 사용하는 사용자가 임의의 명령어를 실행하는 것을 방지.

- authfail_message : 비밀번호 인증에 실패한 경우에 표시되는 메시지를 지정. %d는 실패한 비밀번호 시도 횟수를 나타내는 특수한 플레이스 홀더.

- badpass_message : 잘못된 비밀번호를 입력했을 때 나타나는 오류 메시지.

- log input/log output : sudo를 사용하여 실행한 명령어의 입력과 출력을 로그 파일에 기록

- requiretty : sudo를 사용하여 실행되는 명령이 tty(터미널)가 필요로 하는지를 지정

- iolog_dir : sudo 사용 로그파일의 디렉터리 경로를 지정

- passwd_tries : 사용자가 비밀번호를 재시도 할 수 있는 최대 횟수를 지정

권한 설정

해당 항목은 사용자별로 sudo를 사용할 수 있는 특권을 지정하는 부분으로, 각 줄의 구조는 "사용자명 호스트 = (실행할 사용자 : 실행할 그룹) 실행할 명령"을 의미한다.

따라서 위의 경우 모든 호스트(컴퓨터)에서 모든 사용자/모든 그룹으로 어떤 명령이든 실행 할 수 있도록 권한을 부여한 것이다.

sudo 그룹에 특정 사용자 추가하기

특정 사용자에게 sudo 권한을 부여하려면 '/etc/sudoers' 파일에서 권한 부여를 해주고, sudo group에 추가까지 마쳐야 한다.'

 

4. UFW (Uncomplicated Fire Wall)

- iptable 기반의 간단하고 사용하기 쉬운 방화벽 설정 도구

- 특정 포트, IP 주소 또는 서비스에 대한 접근을 허용하거나 거부하는 규칙을 설정할 수 있음

- 허용되거나 거부된 트래픽에 대한 로그를 기록

ufw를 통해 특정 포트만 열어 둘 수 있다.

- 포트는 컴퓨터와 외부 네트워크간의 통신을 위한 창구로 작동하므로, 특정 포트만 허용함으로써 악의적인 사용자들이 해당 포트를 통해 시스템에 엑세스하거나 공격하는 것을 방지 할 수 있음.

- 트래픽 제어 가능 (예컨대 웹 서버를 운영하는 경우 80번 포트만 허용하여 웹 트래픽을 처리하고, 다른 포트는 차단함으로써 비웹 트래픽을 제한).

 

5. SSH (Secure Shell)

- 네트워크상에서 안전한 원격 접속을 제공하는 프로토콜

- ssh는 데이터 통신을 암호화해서 보호하며, 무결성을 위해 암호화화 함께 메시지 무결성 체크를 사용, 공개키 기반 인증을 사용.

- ssh 나오기 전 사용하기 전 사용하던 telnet의 경우 평문으로 통신하며 무결성 체크를 지원하지 않고, 패스워드 인증을 사용.

/etc/ssh/sshd_config

- ssh 포트를 22번에서 4242번으로 변경 : 많은 공격자들이 기본적으로 사용되는 22번 포트를 대상으로 공격을 시도하므로, 포트 번호를 변경함으로써 보안을 강화

- ssh를 통해 root 권한으로 직접 접속하지 못하게 제한 : root는 시스템에서 최고 관리자 권한을 가지고 있으므로 일반 사용자에 대한 공격보다 훨씬 위험하기 때문에 root로의 직접적인 ssh 접속을 차단

 

6. 비밀번호 정책설정

/etc/login.defs

- 암호는 30일마다 만료

- 비밀번호를 수정하기 전에 허용되는 최소 일 수는 2

- 암호가 만료되기 7일 전에 경고 메시지를 전송

/etc/pam.d/common-password

- 최대 시도 횟수 : 3회

- 최소 길이 : 10

- 기존 암호와 달라야 하는 문자의 개수 : 7

- 대문자 1개 이상 포함, 소문자 1개 이상 포함, 숫자 1개 이상 포함

- 비밀번호에 유저명이 포함되어서는 안 됨

- 루트 계정에도 해당 규칙 적용

- 같은 문자가 3번 이상 반복되면 안 됨

 

* pam_pwquality

- pam(Pluggable Authentication Modules) : Linux 시스템에서 인증과 관련된 작업을 처리하는데 사용되는 프레임워크

- libpam -pwquality :  PAM을 확장하여 패스워드의 품질과 보안을 강화하는데 사용되는 모듈

pam을 사용하기 이전 리눅스 시스템에서는 각 응용 프로그램이 사용자 인증을 위해 자체적으로 로직을 구현하여 사용했기 때문에, 사용자 정보가 담긴 주요 시스템 파일인 '/etc/passwd'에 대한 접근 권한을 가지고 있어야 했다. 그러나 응용 프로그램이 사용자 정보가 담긴 주요 시스템 파일에 대한 직접적인 접근 권한을 가지고 있을 경우 보안과 관련한 위협이 있기 떄문에 pam이 등장하게 되었다.  pam을 사용하면 인증이 필요한 응용 프로그램은 더 이상 passwd 파일을 열람하지 않고 pam 모듈에 사용자 인증을 요청하며, pam은 요청한 사용자의 정보를 가지고 결과를 도출하여 응용 프로그램에 전달한다.

 

7. CRON

Unix 및 Unix 기반 시스템에서 주기적인 작업을 예약하기 위해 사용되는 시간 기반 작업 스케줄러. 'crontab' 파일을 사용하여 작업을 정의하며, 정의된 작업은 지정된 시간 간격에 따라 실행 된다.

crontab -e

- crontab -e 명령어는 crontab 파일을 편집하기 위해 사용되는 명령어.

- crontab -e 는 현재 사용자의 개인적인 crontab 파일을 편집하며, /etc/crontab 파일은 시스템 전체의 cron 작업을 관리.

- 위 경우 10분마다 /home 디렉토리의 monitoring.sh 파일을 출력.

 

1. 가상 메모리 개요

 가상 메모리 시스템이란 물리 메모리의 크기와 상관 없이 프로세스에 커다란 메모리 공간을 제공하는 기술을 의미한다. 가상 메모리에서 메모리 관리자가 사용할 수 있는 메모리의 전체 크기는 물리 메모리와 저장장치(주로 하드디스크)의 스왑 영역이다. 한편 가상 메모리의 메모리 분할 방식에는 가변 분할 방식에 기반한 세그먼테이션 기법과 고정 분할 방식에 기반한 페이징 기법이 존재한다. 전자에서는 절대주소와 상대주소를 사용하는데, 후자의 경우 가상 주소라는 독특한 방식을 사용한다.

 

2. 페이징 기법

1) 개요

페이징 기법

 페이징 기법은 고정 분할 방식을 이용한 가상 메모리 관리 기법으로, 물리 주소 공간을 같은 크기로 나누어서 사용한다. 가상 주소의 분할된 영역은 페이지, 물리 메모리의 각 영역은 프레임이라고 부른다. 각 페이지와 프레임은 번호를 매겨 관리하며, 페이지와 프레임마다 데이터가 저장되어 있다. 이때 페이지와 프레임의 크기는 같다. 페이지 테이블은 하나의 열로 구성되어 있으며 모든 페이지의 내용들이 몇 번 프레임에 저장되어 있는지에 대한 정보를 가지고 있다.

 따라서 페이징 기법에서 프로세스가 가상 주소를 물리 주소로 변환하려고 할 때는 페이지 테이블에서 특정 페이지의 데이터를 읽기 위해 몇 번 프레임으로 가야 하는지 찾아야 한다. 이때 페이지 테이블의 'invalid'는 해당 페이지가 스왑 영역에 있다는 것을 의미한다.

 

2) 페이징 기법의 주소 변환

페이징 기법의 주소 변화

 페이징 기법에서는 가상주소를 VA=<P,D>로 표현한다. VA는 virtual address의 약자이며 P와 D는 page와 distance인데, 여기서 D는 페이지의 처음 위치에서 해당 주소까지의 거리를 의미한다. 페이지 내부에서도 공간이 나뉘기 때문에 페이지 안에서도 몇 번째 위치에 데이터가 있는지를 표시해주어야 한다. 한편 물리주소는 PA=<F,D>로 표현하며 PA는 physical adress, F와 D는 각각 frame과 distance를 의미한다. 프레임도 페이지와 마찬가지로 프레임 내부에서도 공간이 나뉘므로 프레임 안에서 몇 번째 위치에 데이터가 있는지를 표시해야 하므로 distance를 사용한다. 

 프로세스가 가상주소 37번지의 데이터를 찾는다고 가정하자. (37번지는 페이지 3번의 7번째 공간을 의미한다.) 가상주소 37번지는 VA=<3,7>로 표현할 수 있다. 이후 페이지 테이블로 가서 가상주소 페이지 3이 몇 번째 프레임과 매칭되는지를 알아봐야 한다. 페이지 3은 프레임 1과 매칭되므로 프레임 1로 가서 프레임 1의 7번째 공간을 탐색한다. 프레임 1의 7번째 공간은 PA=<1,7>로 표현할 수 있다.

 물론 현실에서는 시스템에 하나의 프로세스만 있는 것이 아니다. 수 많은 프로세스들이 있고 프로세스마다 하나의 페이지 테이블이 있으므로 전체 페이지 테이블의 크기는 프로세스 수에 비례하여 커진다. 페이지 테이블의 크기가 결코 작지 않기 때문에, 페이지 테이블 때문에 정작 프로세스가 사용할 수 있는 용량이 부족한 현상이 나타나기도 한다. 이 경우 페이지 테이블도 스왑 영역으로 옮겨지기도 한다.

 

cf) 페이지 테이블 기준 레지스터

페이지 테이블 기준 레지스터

 프로세스에는 페이지 테이블 기준 레지스터 (Page Table Base Register)가 존재한다. 페이지 테이블 기준 레지스터의 목적은 페이지 테이블에 빠르게 접근하기 위함이며, 여기에는 각 페이지 테이블의 시작 주소가 저장된다. 페이지 테이블 기준 레지스터는 각 프로세스의 프로세스 제어 블록에 존재한다.

 

3) 페이지 테이블 매핑 방식

① 직접 매핑

위에서 설명한 것과 같은 방식이 직접 매핑이다. 페이지 테이블 전체가 물리 메모리의 운영체제 영역에 존재하며 별다른 부가 작업 없이 바로 주소 변환이 가능하다. 직접 매핑은 페이지가 0부터 끝 번호까지 순서대로 나열되어 있으며 각각 프레임 몇 번과 매칭되는지에 대한 정보를 제공한다. 

 

②연관 매핑

연관 매핑

 연관 매핑의 경우 페이지 테이블을 스왑 영역에 둔다. 그리고 페이지 테이블의 일부만 메모리에 가져오는데, 이를 변환 색인 버퍼라고 부른다. 변환 색인 버퍼의 경우 직접 매핑 테이블처럼 페이지 번호 순서대로 정렬 되어 있지 않다는 특징이 있다. 연관 매핑에서는 가상 주소를 물리 주소로 변환하기 위해 우선적으로 변환 색인 버퍼를 탐색하고, 변환 색인 버퍼에 원하는 페이지 번호가 존재하지 않는다면 스왑 영역의 직접 매핑 테이블을 탐색한 뒤 지정된 프레임으로 간다.이때 변환 색인 버퍼에서 바로 원하는 페이지 번호를 찾는데 성공하면 이를 TLB히트라고 하며, 찾지 못하고 스왑 영역으로 가게 되면 이를 TLB 미스라고 한다.   연관 매핑 방식은 전체 페이지 테이블을 물리 메모리에 보관할 필요가 없으므로 메모리를 절약할 수 있다는 장점이 있으나 TLB 미스가 빈번하게 발생하면 시스템 성능이 떨어질 수 있다. 특히 변환 색인 버퍼는 페이지 순서를 무작위로 가지고 있기 때문에 페이지 번호가 변환 색인 버퍼에 있는지 확인하기 위해서는 변환 색인 버퍼의 모든 부분을 찾아봐야 하므로 시간이 상당히 오래 걸린다.

 

③ 집합 - 연관 매핑

집합 - 연관 매핑

 집합 - 연관 매핑은 연관 매핑과 마찬가지로 페이지 테이블을 스왑 영역에 두고 일부만 메모리로 가져온다. 다만 연관 매핑처럼 무작위로 가져오는게 아니라 페이지 테이블을 일정 집합으로 잘라 가져온다. 또한 디렉토리 테이블이라는 새로운 테이블이 추가되었다. 디렉토리 테이블은 일정하게 자른 페이지 테이블이 메모리에 있는지 스왑 영역에 있는지를 나타낸다. 새로운 테이블이 추가되었으므로, 가상주소 역시 VA=<P1,P2,D>로 바뀌었다.

 집합 - 연관 매핑이 어떻게 작동하는지 알아보자. 우선 P1에는 디렉토리의 위치가 들어가고, P2에는 페이지 테이블의 위치, D에는 페이지 내부에서의 위치가 들어간다. 예를 들어 VA=<0,3,2>라면 디렉토리 테이블에서 0을 찾아 테이블 위치가 메모리인지 스왑 영역인지 알아본다, invalid면 스왑 영역으로 가서 페이지를 찾는데, 0이 있으므로 스왑 영역까지 갈 필요가 없다. 디렉토리 테이블 0번에서 필요한 페이지 테이블의 시작 주소를 찾는다. 그리고 필요한 테이블 위치로 가서 P2 값을 이용하여 원하는 프레임 번호를 얻고, Distance번째 공간에 가서 데이터를 가져오면 된다.

 

3. 세그먼테이션 기법

1) 개요

 

세그먼테이션 기법

 세그먼테이션 기법은 가변 분할 방식을 이용한 가상 메모리 관리 기법으로, 물리 메모리를 프로세스의 크기에 따라 가변적으로 나누어 사용한다. 세그먼테이션 기법에서도 매핑 테이블을 사용하는데, 이를 세그먼테이션 테이블이라 한다. 세그먼테이션 테이블에는 세그먼트의 크기를 나타내는 Limit과 물리 메모리상의 시작 주소를 나타내는 Address가 있다. 예를 들어 세그먼트 0은 물리 메모리의 120번지에서 시작하여 280만큼의 크기를 사용하므로 Address와 Limit이 각각 120, 280이 된다. 세그먼트 0이 끝나는 지점은 120에 280을 더한 400번지이다. (세그먼트 변호는 테이블 윗쪽부터 순서대로 매칭되므로 생략한다.)

 

3) 세그먼테이션 기법의 주소 변환

세그먼테이션 기법의 주소 변환

 세그먼테이션 기법에서는 가상 주소를 VA=<S,D>로 표현한다. S는 segment를 의미하고 나머지는 페이징 기법과 같다. 가상 주소를 부여받으면 세그먼테이션 테이블에 가서 Address를 확인한다. 그리고 Address에 Distance 값을 더해서 물리 주소를 찾는다. 이때 만약 Address+Distance 값이 Limit을 넘어간다면 주소 오류이다. 

한빛출판사 정성호 저 <쉽게 배우는 운영체제>를 참고하여 작성하였습니다.

1. 메모리 관리란?

 컴퓨터의 운영체제를 포함한 모든 응용 프로그램은 실행 되기 위해 메모리에 올라와야 한다. 이때 메모리의 작업 공간은 한정 되어 있으므로 메모리 공간을 잘 관리하는 것은 아주 중요한 문제이다. 메모리 관리응 메모리 관리자가 담당하는데, 메모리 관리자는 프로세스가 필요로 하는 데이터를 메모리로 언제 가져올지, 메모리의 어느 부분에 배정할지, 메모리의 공간이 부족할 때 어느 프로세스를 언제 내보내야 할 것인지를 결정한다.

2. 메모리 주소

1) 운영체제 영역과 사용자 영역

 메모리 영역은 운영체제 영역과 사용자 영역으로 나눌 수 있다. 운영체제 영역은 시스템을 관리하는 중요한 역할을 하므로, 사용자가 사용자 영역에만 접근하고 운영체제 영역에는 접근하지 못하도록 메모리를 관리해야 한다.

 

메모리

 위 예시 메모리에서는 0~100번지까지를 운영체제 영역으로, 101~400번지를 사용자 영역으로 사용하고 있다. 이때 사용자 프로세스는 101번지부터 적재되어야 할 것이다. 그러나 이러한 방식으로 배치를 하게 되면 운영체제 영역이 메모리를 더 필요로 할 때마다 사용자 영역의 시작 위치가 달라질 것이다. (예컨대 갑자기 운영체제 영역이 150번지까지 필요로 하면 사용자 영역의 시작 위치를 151번으로 바꾸어야 할 것이다.) 이러한 문제를 해결하기 위한 방법 중 메모리를 거꾸로 사용하는 방법이 있다. 즉 사용자 프로세스를 101번이 아닌 400번부터 시작하여 399번, 398번.. 과 같이 거꾸로 배치하는 것이다. 그러나 이 방법 역시 메모리를 거꾸로 사용하기 위해 주소를 변경하는 것이 번거롭다는 단점이 있다.

경계 레지스터

  사용자 영역이 운영체제 영역을 침범하지 않게 하기 위해서 경계 레지스터를 사용한다. 경계 레지스터는 CPU에 위치하며, 말 그대로 운영체제 영역과 사용자 영역의 경계 지점의 주소를 가진다. 메모리 관리자는 사용자가 작업을 요청할 때마다 경계 레지스터의 값을 넘어가지는 않는지 체크하고, 경계 레지스터의 값을 넘어갈 시 프로세스를 종료한다.

 

2) 절대주소와 상대주소

절대주소와 상대주소

 절대주소는 물리주소라고도 하는데, 메모리 관리자 입장에서 바라본 주소이다. 그러나 사용자 입장에서는 운영체제는 어차피 사용하지 못하는 공간이므로 절대주소를 사용할 필요 없이 사용자 영역의 시작점을 0으로 두고 주소를 세도 되는데 이를 상대주소라고 한다. 이때 절대 주소를 사용하는 공간을 절대 주소 공간이라하고 상대 주소를 사용하는 공간을 논리 주소 공간이라고 부른다.

 메모리에 접근할 때 절대 주소를 사용하면 바로 위치를 찾아 작업을 할 수 있지만, 상대 주소를 사용하면 상대주소를 절대주소로 변환하는 과정이 필요하다. 상대 주소를 절대 주소로 변환은 재배치 레지스터를 이용하여 이루어진다. 프로세스가 상대주소 100번지에 있는 데이터를 요청하면, cpu가 메모리 관리자에게 100번지에 있는 내용을 가져오라고 명령하고, 메로리 관리자는 운영체제가 끝나는 지점인 100번지를 상대주소 100번지에 더해 절대주소가 되는 200번지로 변환한다.

 

3. 메모리 오버레이와 스왑

메모리 오버레이와 스왑은 메모리에 한 번에 하나의 프로세스만 올리는 단순 메모리 구조에서 사용하는 개념이다. 메모리에 한 번에 여러 프로세스를 올릴 때 사용하는 개념인 메모리 분할 방식은 4번에서 본격적으로 알아볼 것이다.

 

1) 메모리 오버레이(memory overlay)

 프로그램의 크기가 클 때 전체 프로그램을 메모리에 가져오는 대신 적당한 크기로 잘라서 가져오는 기법을 메모리 오버레이라고 한다. 전체 프로그램의 크기가 너무 커서 이를 모두 메모리에 올려 놓기 부담스러울 경우에 실행하는데 중요한 모듈만 올려놓고 나머지는 필요할 때마다 메모리에 가져와 사용하는 것이다. 메모리 오버레이를 사용하면 한정된 메모리보다 더 큰 프로그램도 실행할 수 있다.

 

2) 스왑(swap)

모듈 A를 메모리에서 내보내고 모듈 B를 메모리로 스왑인

  메모리 오버레이를 이용하면 전체 프로세스를 메모리에 올리지 않아도 실행이 가능하다고 하였다. 위 그림은 공통 모듈과 모듈 A, 모듈 B로 이루어진 프로세스를 메모리 오버레이를 이용하여 실행하는 장면이다. 모듈 A가 메모리에 올라와 있다가, 모듈 B를 사용하기 위해 모듈 A를 하드디스크로 내보내고 모듈 B를 메모리에 올린 것이다. 이때 쫓겨난 모듈A는 하드디스크의 스왑 영역으로 이동한다. 아직 프로세스의 작업이 끝나지 않았고, 언제 다시 사용할지 모르기 때문에 하드디스크의 아무 곳에나 놓지 않고 스왑 영역이라는 별도의 공간에 보관한다. 이때 스왑 영역에서 데이터를 가져오는 작업을 스왑인이라고 하며 스왑 메모리에서 스왑 역역으로 데이터를 내보내는 작업을 스왑 아웃이라고 한다.

 

4. 메모리 분할 방식

메모리 분할 방식에는 크게 가변 분할 방식과 고정 분할 방식이 있다.  이 둘은 메모리에 한 번에 여러 프로세스를 올릴 때 사용하는 개념으로, 가변 분할 방식은 프로세스의 크기에 따라 메모리를 나누는 것을 의미하며 고정 분할 방식은 프로세스의 크기와 상관 없이 메모리를 같은 크기로 나누는 것이다. 두 방법 모두 장단점이 존재한다.

 

1) 가변 분할 방식

가변 분할 방식

가변 분할 방식은 프로세스의 크기에 따라 메모리를 나누는 것이다. 따라서 첫번째 메모리를 보면 프로세스가 메모리에 빈 공간 없이 들어찬 것을 확인해볼 수 있다. 이렇게 프로세스를 한 덩어리로 처리하여 하나의 프로세스를 연속된 공간에 배치할 수 있다는 것이 가변 분할 방식의 장점이다.

 그런데 만약 몇몇 프로세스들이 작업을 마치고 메모리에서 빠져나가면 두번째 메모리와 같은 모습으로 변한다. 이런 식으로  가변 분할 방식에서 메모리에 빈 공간이 발생하는 것을 외부단편화라고 하는데, 이 경우 프로세스 B와 프로세스 D보다 큰 프로세스는 메모리에 들어올 수 없게 된다. 이 경우  조각 모음을 이용하여 문제를 해결한다. 조각 모음이란 외부 단편화로 발생한 작은 조각들을 모아서 하나의 큰 덩어리로 만드는 작업을 말한다. 만약 두번째 메모리와 같이 외부 단편화가 발생한 시점에서 조각모음을 시행하면 마지막 메모리와 같은 모습이 될 것이다.

 

cf) 메모리 배치 방식

가변 분할 방식에는 최초 배지, 최적 배치, 최악 배치, 버디 시스템이라는 배치 방식이 존재한다. 필요에 따라 다양한 메모리 배치 방식을 사용하여 메모리 효율성을 높이는 것이 중요하다.

① 최초 배치

 최초 배치는 프로세스를 메모리의 빈 공간에 배치 할 때 메모리에서 적재 가능한 공간을 순서대로 찾다가 첫 번째로 발견한 빈 공간에 프로세스를 배치하는 방법이다.

② 최적 배치

 최적 배치는 메모리의 빈 공간을 모두 확인한 후 메모리가 들어갈 수 있는 공간 중 가장 작은 공간에 프로세스를 배치하는 방법이다. 이 경우 딱 맞는 공간이 있다면 외부 단편화가 일어나지 않을 것이다.

③ 최악 배치

 최악 배치는 최적 배치와 반대로 빈 공간을 모두 확인한 후 가장 큰 공간에 프로세스를 배치하는 방법이다.이러한 방법을 빈 공간의 크기가 넉넉할 때 사용한다면 작은 조각을 만들어내지 않으므로 효과적일 수 있다.

 

2) 고정 분할 방식

고정 분할 방식

고정 분할 방식은 프로세스는 일정한 크기대로 메모리를 나누는 메모리 분할 방식이다. 만약 한 공간 안에 하나의 프로세스가 들어가지 않으면 다른 공간에 나누어 배치한다. 따라서 위 그림처럼 같은 프로세스 A라도 서로 떨어진 메모리 공간에 배치 될 수 있다. 메모리를 일정한 크기로 나누어 관리하기 때문에 조각 모음과 같은 부가적인 작업을 할 필요가 없다는 것이 고정 분할 방식의 장점이다.

 하지만 나누어진 공간보다 프로세스의 크기가 작은 경우 위처럼 내부 단편화가 발생하면서 공간이 낭비되기도 한다는 단점이 있다.

 

3) 버디 시스템

버디 시스템

  버디 시스템은 가변 분할 방식과 고정 분할 방식이 합쳐진 방법으로 프로세스의 크기에 맞게 메모리를 1/2로 자르고 프로세스를 메모리에 배치하는 방법이다. 나뉜 구역에는 프로세스가 한 개씩만 들어간다. 이 과정에서 나뉜 구역보다 프로세스의 크기가 작아 위 메모리 그림의 오른쪽 끝처럼 내부 단편화가 발생하기도 한다. 이 때, 프로세스가 종료되면 주변의 빈 조각끼리 합쳐져 하나의 큰 덩어리가 된다.

 버디 시스템은 가변 분할 방식과 달리 비슷한 조각이 서로 모여 있기 때문에 작은 조각을 합쳐 큰 덩어리를 만드는 것이 쉽다는 장점이 있다. 가변 분할 방식의 경우 여러 크기의 조각들이 중구난방으로 흩어져 있어 조각 모음을 위해 큰 덩어리를 옮겨야 할 때가 많으나 버디 시스템은 그렇지 않다. 즉 버디 시스템이 가변 분할 방식보다 메모리 공간 관리가 더 편리하다.

한빛출판사 정성호 저 <쉽게 배우는 운영체제>를 참고하여 작성하였습니다.

1. 프로세스 간 통신

1) 개요
한 프로세스가 같은 컴퓨터 내의 프로세스끼리 혹은 네트워크로 연결 된 다른 컴퓨터에 있는 프로세스와 데이터를 주고 받는 것을 프로세스 간 통신 (IPC:Inter-Process Communication) 이라고 한다.

프로세스 내부 데이터 통신 하나의 프로세스 내에 2개 이상의 스레드가 존재하는 경우의 통신
프로세스 간 데이터 통신 같은 컴퓨터의 프로세스끼리 통신
네트워크를 이용한 데이터 통신 여러 컴퓨터가 네트워크로 연결 되어 있을 때 통신

프로세스간 통신의 종류는 위 처럼 크게 세 가지로 나눌 수 있으며, 프로세스 내부 데이터 통신의 경우는 전역 변수나 파일을 이용해 이루어지며, 프로세스 간 데이터 통신은 공용 파일 혹은 파이프를 이용하여 통신이 이루어진다. 서로 다른 기기가 네트워크를 이용하여 통신을 할 때는 주로 소켓을 이용하여 통신을 한다.
전역 변수나 파일, 파이프를 이용한 통신은 단방향 통신으로 한쪽 방향으로만 데이터를 전송할 수 있으며 소켓의 경우는 양방향 통신으로, 데이터를 동시에 양쪽 방향으로 동시에 전송 할 수 있다.

2) 프로세스 간 통신의 종류 상세
① 전역 변수를 이용한 통신

전역 변수를 이용한 통신

전역 변수를 이용한 통신은 공동으로 관리하는 메모리를 사용하여 데이터를 주고 받는 방법이다. 데이터를 보낼 때는 쓰기 연산을 통해 전역 변수에 값을 쓰고, 받는 쪽에서는 읽기 연산을 통해 전역 변수의 값을 읽어온다.
전역 변수를 이용하여 통신할 때는 단방향으로만 통신이 가능하다. 따라서 양방향으로 통신하기 위해서는 경우를 프로세스 A에서 프로세스 B로 통신할 때와 프로세스 B에서 프로세스 A로 통신할 때 두 가지로 나누어야 하고, 전역변수 역시 두 개가 필요하다.
이 때 데이터를 받는 입장에서는 전역 변수의 값을 제때 읽지 못하면 보내는 입장에서 전역 변수에 값을 덮어 쓸 수 있기 때문에 짧은 주기로 전역 변수를 계속해서 확인해야 한다.

② 파일을 이용한 통신

파일을 이용한 통신

파일을 이용한 통한 통신의 주요한 연산에는 파일 열기(open()), 파일 닫기(close()), 파일 쓰기(write()), 파일 읽기(read())가 있다. 프로세스가 입출력 관리 프로세스에 쓰기를 요구하면 데이터가 저장되고, 읽기를 요구하면 입출력 관리 프로세스로부터 데이터를 가져온다.
파일을 이용한 통신은 운영체제가 동기화를 제공하지 않아 프로세스가 스스로 동기화를 해야하는데, 이떄 wait() 함수를 이용한다.

③ 파이프를 이용한 통신

파이프를 이용한 통신

파이프를 이용한 통신은 open() 연산으로 기술자를 얻고 작업을 한 후 close() 연산으로 마무리 한다는 점에서는 파일을 이용한 통신과 유사하지만, 파일을 이용한 통신과 다르게 운영체제가 동기화를 지원한다. 또한 단방향 통신 방식으로, 양방향 통신을 하기 위해서는 파이프가 두 개 필요하다.
데이터를 보내는 프로세스는 파이프에 쓰기 연산을 통해 전송하고, 받는 프로세스는 읽기 연산을 통해 받는다. 예를 들어 프로세스 A가 프로세스 B에 데이터를 보낸다고 가정하면, 프로세스 A가 파이프 1에 쓰기 연산을 하고 프로세스 B는 파이프 1의 데이터를 읽는다. 프로세스 B는 파이프 1의 데이터를 읽은 후 대기 상태로 진입하고, 프로세스 A가 다시 파이프 1에 데이터를 쓰면 대기 상태가 풀리면서 동기화가 이루어진다. 이러한 방식으로 동기화가 지원되므로, 전역 변수를 이용한 통신처럼 데이터를 받는 프로세스가 파이프를 바쁘게 확인해 줄 필요가 없다.

④ 소켓을 이용한 통신

소켓을 이용한 통신

소켓을 이용한 통신은 서로 다른 기기에 존재하는 프로세스가 네트워크를 이용하여 통신할 때 사용하는 방법이다. (소켓은 프로세스가 네트워크를 통해 데이터를 보내기 위한 창구 역할을 한다.) 이들은 통신을 하기 위해 자신의 소켓과 상대의 소켓을 연결하고, 이를 통해 프로세스끼리도 연결이 되는데 이러한 작업을 바인딩(binding) 이라고 한다. 프로세스가 소켓을 바인딩한 후 소켓에 쓰기 연산을 하면 데이터가 전송되고, 읽기 연산을 하면 데이터를 받을 수 있다.
한편 소켓은 프로세스 동기화를 지원하므로 전역 변수를 이용한 통신처럼 프로세스가 짧은 주기로 계속해서 소켓을 확인할 필요가 없다.

2. 공유자원과 임계구역

1) 개요
공유 자원은 여러 프로세스가 공동으로 이용하는 변수, 메모리, 파일 등을 의미한다. 공유 자원은 공동으로 이용되므로 누가 언제 데이터를 읽거나 쓰느냐에 따라 결과가 달라 질 수 있으므로 자원 접근 순서를 잘 조정하여 의도치 않게 결괏값이 바뀌는 상황을 방지하여야 한다.
임계구역은 공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역을 의미한다. 임계 구역에서는 프로세스들이 동시에 작업해서는 안 되며, 어떤 프로세스가 한 번 임계구역에 진입하면 작업이 끝날 때까지 다른 프로세스가 임계구역에 들어오는 것을 막아야 한다.
임계구역을 안전하게 보호하기 위해서는 상호배제(한 프로세스가 임계 구역에 들어가면 다른 프로세스는 임계 구역에 들어갈 수 없음), 한정 대기(한 프로세스가 무한 대기에 빠지면 안 됨), 진행의 융통성(한 프로세스가 다른 프로세스의 작업을 방해해서는 안 됨) 조건을 충족해야 한다.

2) 임계 구역의 종류
① 전역 변수로 잠금을 구현
전역 변수로 잠금을 구현하는 첫 번째 방법이다.

전역 변수로 잠금을 구현 - 1

프로세스 A와 B는 공유변수 'lock=false'를 공유한다. 프로세스 A와 B는 모두 'lock=false' 인 상태에서 임계구역에 진입할 수 있으며, 진입한 뒤에 'lock=true' 를 실행하여 다른 프로세스가 임계구역에 진입하지 못하도록 한다.
그러나 동시 진입 상황이 발생하는 경우가 있다. 프로세스 A가 'while(lock==true)' 구문을 반복해서 실행하다가 'lock=false' 조건이 발생하여 반복문을 끝냈다고 가정하자. 그런데 이 때 프로세스 A가 'lock=true;' 구문을 실행하기 전 타임아웃이 발생하고, 이 상태에서 프로세스 B가 'while (lock==true);' 문을 끝내고 'lock=true;'를 걸고 임계구역에 진입하였다. 그리고 타임아웃이 끝난 뒤 프로세스 A도 'lock=true;'를 걸고 임계구역에 진입하였다. 이 경우, 프로세스 A와 B가 모두 임계구역에 동시에 진입하게 된다. 상호 배제 조건이 위배되는 것이다.

전역 변수로 잠금을 구현하는 두 번째 방법이다.

전역 변수로 잠금을 구현 -2

프로세스 A와 B는 공유 변수 'lock_A=false;'와 'lock_B=false;'를 공유한다. 프로세스 A와 B는 진입하기 전에 먼저 'lock_A(또는 B)=true' 문을 이용하여 잠금을 설정한다. 첫 번째 방법과는 다르게 일단 잠금을 하고 다른 프로세스가 잠금이 걸렸는지 확인하므로 동시에 진입하여 문제가 발생할 걱정이 없다. 그러나 위와 같은 방법에서는 또 다른 문제가 발생한다.
이러한 방법에서는 무한 루프 문제가 발생한다. 무한 루프 문제는 프로세스 A는 'lock_A=true'문을, 프로세스 B는 'lock_B=true' 구문을 수행하고 각각 타임아웃에 빠질 경우에 발생한다. 이러한 상황에서는 프로세스 A와 B 모두 계속해서 임계구역에 진입하지 못하고 반복문만 돌고 있게 된다.

전역변수로 잠금을 구현하는 세 번째 방법이다.

전역 변수로 잠금을 구현 -3

세 번째 방법은 비교적 간단한다. 최초에 'lock=1;' 이라는 공유변수를 공유한다. 이때 프로세스 A가 반복문을 끝내고 진입하여 작업을 한 뒤 'lock=2;' 구문을 실행하고, 그 뒤 프로세스가 반복문을 끝내고 진입하여 작업을 수행한 뒤 'lock=1;' 구문을 실행하고 또 다시 프로세스 A가 작업을 수행하는 식이다. 이 경우 상호 배제와 한정 대기 조건을 모두 충족하게 되지만 같은 프로세스가 연속적으로 임계구역에 진행할 수 없다는 문제점이 있다.

② 피터슨 알고리즘

피터슨 알고리즘

피터슨 알고리즘은 임계구역 문제 해결에 필요한 세 가지 조건 (상호 배제, 한정 대기, 진행의 융통성)을 모두 충족한다. 단, 두 개의 프로세스에서만 사용이 가능하며 세 개 이상의 프로세스부터는 적용이 불가능하다는 한계점이 있다.
피터슨 알고리즘은 전역 변수로 잠금을 구현의 두 번째 방법과 매우 유사한데, 여기에 무한 루프에 빠지는 상황을 대비하여 'turn'이라는 변수를 하나 더 추가했다는 차이점이 있다.
공유변수 'int turn='A';'을 가진 상태에서 두 프로세스가 프로세스 A, B의 차례로 lock을 걸고 타임아웃에 처했다고 가정하자. turn 변수가 없다면 두 프로세스 모두 무한 루프에 빠져 작업을 할 수 없는 상태가 될 것이다. 그러나 turn 변수를 추가함으로서 무한 루프에 빠지지 않고 다른 프로세스에게 순서를 양보하며 작업 처리가 가능하다. (물론 프로세스 A와 B가 'turn='N';' 구문까지 실행하고 타임아웃에 처했는지 아닌지에 따라 어떤 프로세스가 먼저 임계구역에 진입하는지가 달라지기는 하겠다.)

③ 데커 알고리즘

데커 알고리즘

데커 알고리즘 역시 피터슨 알고리즘과 마찬가지로 임계 구역 문제 해결에 필요한 세 가지 조건을 모두 만족한다. 다만 과정이 복잡하여 잘 사용하지는 않는다. (피터슨 알고리즘도 마찬가지이다.) 프로세서 A 입장에서의 데커 알고리즘의 전개 과정을 순서도로 나타내보면 아래와 같다.

프로세스 A 입장에서의 데커 알고리즘 전개 과정

만약 프로세스 A가 임계구역에서 작업을 끝마치고 나면 'turn='B';' 문장을 통해 프로세스 B에게 순서를 넘겨준 뒤 'lock_A='false';' 문장을 통해 프로세스 A의 잠금을 해제할 것이다. 그리고 이후 프로세스 B가 임계구역에 진입할 수 있게 된다.


④ 세마포어 (Semaphore)
세마포어 역시 임계 구역이나 공유 자원에 여러 프로세스가 동시에 접근하여 혼란을 초래하는 것을 막는 방법 중 하나인데, 위에서 서술한 방법보다 비교적 간단하며 사용하기도 쉽다.

세마포어의 작동 과정

RS는 임계 구역에 동시 진입이 가능한 프로세스의 수를 의미한다. 따라서 처음에 세마포어를 설정할 때 RS의 수를 입력받는다. P();는 잠금을 수행하는 코드이다. 프로세스가 P();를 통해 임계구역에 진입할 때, RS의 값이 0보다 크면 RS의 수를 1 감소시키고 임계구역에 진입하며, 만약 RS가 0보다 작거나 같으면 진입하지 않고 대기한다. V();는 잠금을 해제하고 동기화를 진행하는 코드로, 프로세스는 임계구역에서 나올 때 V();를 통해 빠져나오며 RS를 1 증가시킨다.

1. CPU 스케줄링의 개요

 CPU 스케줄링은 특정 프로세스에 편중되지 않게 자원을 배분하기 위해 사용한다. 

 

 1) CPU 스케줄링의 단계

고수준 스케줄링 시스템 내의 전체 작업 수를 결정 (프로세스의 시작 여부를 결정)
중수준 스케줄링 전체 시스템의 활성화 된 프로세스 수를 조절 (과부하 우려 시 실행 중인 프로세스를 보류상태로)
저수준 스케줄링 프로세스에 CPU 할당 및 준비 상태와 대기 상태 관리 등 (프로세스 상태전이도 내용에 해당)

 2)  CPU 스케줄링의 구분

선점형 스케줄링 어떤 프로세스가 CPU를 할당받아 실행 중이더라도 다른 프로세스가 CPU를 빼앗아 차지할 수 있는 스케줄링 기법
비선점형 스케줄링 어떤 프로세스가 CPU를 할당받아 실행 중일땐 다른 프로세스가 CPU를 빼앗아 차지하지 못하는 스케줄링 기법

 선점형 스케줄링의 경우 프로세스가 CPU를 독점할 수 없어 시분할시스템에 적합하나, 문맥 교환의 오버헤드가 많다. 반면 비선점형 스케줄링의 경우 CPU 스케줄러의 작업량이 적고 문맥교환도 적기 때문에 오버헤드가 적지만 기다리는 프로세스가 많아 처리율이 떨어진다.

 

 2)  CPU 스케줄링 시 프로세스의 우선순위 결정

 일반적으로 프로세스의 우선순위는 다음과 같다.

① 커널 프로세스 > 일반 프로세스

② 입출력 집중 프로세스 > CPU 집중 프로세스

③ 전면 프로세스 > 후면 프로세스

④ 대화형 프로세스 > 일괄 처리 프로세스

 

 프로세스의 우선순위를 결정하는 방식으로는 다음과 같은 종류가 있다.

고정 우선순위 방식 한 번 프로세스에 우선순위가 부여되면 프로세스 작업이 끝날 때까지 변하지 않는 방식
변동 우선순위 방식 프로세스에 우선순위가 부여되었더라도 프로세스 작업 중간에 우선순위 변동이 가능한 방식

 고정 우선순위 방식은 구현은 쉽지만 작업 효율이 떨어지고, 변동 우선순위 방식은 구현은 어려우나 작업 효율이 고정 우선순위 방식보다 상대적으로 높다.

 

2. CPU 스케줄링의 종류

1) 선입선출 스케줄링 (FCFS, FIFO)

선입선출 스케줄링

 선입선출 스케줄링은 준비 큐에 도착한 순서대로 CPU를 할당한다. 비선점형 방식으로, 처리 시간이 긴 프로세스가 CPU를 차지하면 다른 프로세스는 계속해서 대기해야 하므로 효율성이 낮은 편이다.

 

2) 최단 시간 우선 스케줄링 (SJF)

최단 시간 우선 스케줄링

 최단 시간 우선 스케줄링은 준비 큐에 있는 프로세스 중에서 실행 시간이 가장 짧은 작업부터 CPU를 할당하며, 비선점형 방식이다. 선입선출 스케줄링보다 효율성은 높으나, 작업 시간이 길다는 이유로 계속해서 뒷순위로 밀리는 프로세스가 생길 수 있다. 이러한 현상을 아사(Starvation) 현상이라고 부른다. 아사현상 완화를 위해서는 프로세스가 자신의 순서를 양보할 때마다 나이를 먹고 나이를 먹을 수록 우선순위를 높이는 에이징(aging) 기법을 사용할 수 있다.

 

3) 최고 응답률 우선 스케줄링 (HRN)

우선순위 = ( 대기시간 + CPU 사용시간 ) / CPU 사용시간

 최고 응답률 우선 스케줄링은 최단 시간 우선 스케줄링에서 발생하는 아사 현상을 해결하기 위해 만들어졌으며, 비선점형 방식이다. 최단 시간 우선 스케줄링과 같은 방식으로 작동하나, 최단 작업이 아닌 위와 같은 방법으로 우선 순위를 결정하고 우선 순위가 높은 프로세스를 우선적으로 처리한다.

 

4) 라운드 로빈 스케줄링 (RR)

라운드 로빈 스케줄링

라운드 로빈 스케줄링은 선입 선출 스케줄링과 유사하게 한 프로세스가 할당받은 시간(타임 슬라이스)동안 작업을 하다가 작업을 완료하면 완료 상태로, 그렇지 못하면 다시 준비 큐의 맨 뒤로 가서 자기 차례를 기다리는 스케줄링 기법이며, 선점형 방식이다. 타임 슬라이스가 지나치게 크면 선입 선출 스케줄링과 다를 바가 없어지고 타임슬라이스가 지나치게 작으면 문맥 교환이 너무 자주 일어나 효율성이 떨어지므로 적절한 타임 슬라이스를 정해주는 것이 중요하다.

 

5) 최소 잔여시간 우선 스케줄링 (SRT)

 

최소 잔여시간 우선 스케줄링

 최소 잔여시간 우선 스케줄링은 최단 시간 우선 스케줄링을 선점 형태로 변형한 기법이다. 준비 큐 중 가장 빨리 작업을 끝낼 수 있는 프로세스에게 CPU를 먼저 할당하고, 중간에 들어온 새로운 프로세스가 더 빠른 시간 내에 작업을 마칠 수 있다면 실행 중이던 프로세스를 타임아웃 시키고 새로운 프로세스에 CPU를 할당한다.

 최소 잔여시간 우선 스케줄링은 프로세스의 종료 시간을 예측하기 어렵고, 최단 시간 우선 스케줄링와 마찬가지로 아사 현상이 일어나기 때문에 잘 사용하지 않는다.

 

 6) 다단계 큐 스케줄링 (MLQ)

다단계 큐 스케줄링

 다단계 큐 스케줄링은 우선순위에 따라 준비 큐를 여러개 사용하는 스케줄링 방식이다. 프로세스는 운영체제로부터 부여 받은 우선순위에 따라 해당 우선순위의 큐에 삽입되며, 상위 큐에 있는 모든 프로세스의 작업이 끝나야 다음 우선순위 큐의 작업이 시작된다. 각 단계에서는  주로 라운드 로빈 스케줄링의 방식을 사용한다. 그러나 이러한 방식은 우선순위가 높은 프로세스 때문에 우선순위가 낮은 프로세스의 작업이 계속해서 연기되는 문제가 발생할 수 있다.

 

 6) 다단계 피드백 큐 스케줄링 (MLFQ)

 

다단계 피드백 큐 스케줄링

다단계 피드백 큐 스케줄링은 우선순위가 낮은 프로세스의 작업이 계속해서 지연되는 다단계 큐 스케줄링의 문제점을 보안하기 위해 고안되었다. 기본적인 매커니즘은 다단계 큐 스케줄링과 유사하나, 다단계 피드백 큐 스케줄링은 CPU를 사용하고 난 프로세스의 우선순위가 낮아진다는 차이점이 있다. 즉 CPU를 사용하고 난 프로세스는 원래의 큐가 아니라 우선순위가 하나 낮은 큐의 끝으로 들어간다. 물론 우선순위가 낮아진다고 하더라도 커널 프로세스의 우선순위가 일반 프로세스의 큐로 들어가는 일은 없다. 또한, 일반적으로 다단계 피드백 큐 스케줄링은 우선순위가 낮아질수록 실행 기회를 더 많이 주기 위해 타임 슬라이스를 크게 배정한다. (가장 낮은 우선순위의 경우 선입선출 스케줄링과 같은 방식으로 작동한다.)

한빛출판사 정성호 저 <쉽게 배우는 운영체제>를 참고하여 작성하였습니다.

1. 프로세스의 개념

1) 프로세스의 정의

 저장장치에 저장되어 있던 프로그램이 실행을 위해 메모리에 올라오면 프로세스가 된다. 즉 프로그램은 정적인 상태이며 프로세스는 동적인 상태라고 할 수 있다.

 프로그램이 프로세스로 전환될 때, 운영체제는 프로그램을 메모리의 적당한 위치로 가져온 뒤에 작업 지시를 하는데 이 작업지시는 프로세스 제어 블록 (Process Control Block,PCB)을 통하여 이루어진다. 즉 프로그램이 프로세스가 되었다는 것은 운영체제로부터 프로세스 제어 블록을 받았다는 것을 의미한다. 이때 프로세스 제어 블록은 운영체제가 해당 프로세스를 위해 관리하는 데이터 구조이기 때문에 운영체제 영역에 만들어진다.

프로그램이 프로세스로 전환

2) 프로세스의 구조

 프로세스는 코드 영역, 데이터 영역, 스택 영억으로 구서오딘다. 코드 영역은 프로그램의 본문이 기술된 곳으로 읽기 전용으로 처리 된다. 데이터 영역은 코드가 실행되면서 사용하는 변수나 파일 등의 각종 데이터를 모아놓은 곳이다.  데이터는 변화하는 값이므로 읽기모드와 쓰기모드를 모두 지원한다. 마지막으로 스택영역은 운영체제가 프로세스를 실행하기 위해 필요한 데이터를 모아 놓은 곳으로, 사용자에게는 보이지 않는다.

프로세스의 구조

3) 프로세스 제어 블록 (PCB)

  프로세스 제어 블록은 프로세스를 실행하는데 필요한 정보를 보관하는 자료 구조로, 프로세스는 각각 고유의 프로세스 제어 블록을 가진다. 프로세스 생성 시 만들어져서 프로세스 실행이 끝나면 폐기된다.

프로세스 제어 블록의 구조

 위 그림은 프로세스 제어 블록의 구조이다. 포인터는 준비 상태나 대기 상태의 큐(Queue)를 구현할 때 사용된다. 준비상태에서는 CPU 스케줄링 기법에 따라 포인터를 이용하여 프로세스의 처리 순서를 정하며, 대기 상태에서는 같은 입출력을 요구한 프로세스끼리 연결할 때 사용된다. 그 외에도 프로세스 구분자, 프로세스 상태, 프로세스 카운터(다음 실행 될 명령어의 위치를 저장) 프로세스 우선순위, 레지스터와 메모리 및 할당 자원의 정보, 계정 정보 등을 포함한다. PPID와 CPID는 각각 parent process ID, Child process ID의 준말로 부모 프로세스와 자식 프로세스의 정보를 저장한다.

2. 프로세스의 상태

1) 프로세스 상태전이도

프로세스 상태전이도

 생성 상태는 프로세스가 메모리에 올라와 실행 준비를 완료한 상태이다. 프로세스는 프로세스 제어 블록(PCB)를 얻어 준비 상태로 진입한다. 준비 상태에서 프로세스는 CPU를 할당 받을 때까지 대기하며, CPU를 할당 받으면 실행 상태로 진입하여 작업을 수행한다. 이때 만약 제한된 시간 안에 작업을 완료하면 프로세스 제어 블록을 반환한 뒤 완료 상태로 진입하며, 그렇지 못할 경우 준비 상태로 되돌아가 다시 자신의 차례를 기다린다.

 만약 실행상태에서 프로세스가 입출력을 요청 받으면 대기 상태로 진입하여 사용자의 입출력을 기다리며, 입출력이 완료 되면 다시 준비상태로 진입한다.

 보류상태는 메모리에서 프로세스가 쫓겨나 하드디스크에서 대기하는 상태를 말한다. 메모리가 꽉 차서 일부 프로세스를 밖으로 내보내거나, 프로그램 오류가 발생했을 때, 입출력이 계속 지연될 때 등의 경우에 프로세스는 보류 대기 상태로 진입한다.

 

2) 문맥교환

 문맥교환은 CPU를 차지하던 프로세스가 나가고 새로운 프로세스를 받아들이는 작업을 의미한다. 즉 상태전이도에서 준비 상태와 실행 상태의 사이에서 일어나는 현상이다. 실행 상태에서 나가는 프로세스는 프로세스 제어 블록에 지금까지의 작업 내용을 저장하고, 준비 상태에서 실행 상태로 들어오는 프로세스의 경우에는 프로세스 제어 블록의 내용을 바탕으로 CPU를 다시 세팅한다.

3. 프로세스의 생성과 복사

1) fork()

  fork() 시스템 호출은 실행 중인 프로세스로부터 새로운 프로세스를 복사하는 함수로, 실행하던 프로세스는 부모 프로세스가 되고 새로 생긴 프로세스는 자식 프로세스가 된다. 부모 프로세스 영역의 대부분이 자식 프로세스에 복사되어지며, 프로세스 제어 블록의 프로세스 구분자와 메모리 관련 정보, PPID, CPID가 변경된다. fork() 시스템 호출을 사용하면 하드디스크에서 새로 프로그램을 가져오는 것이 아니고 기존 메모리에서 복사하므로 프로세스 생성 속도가 빠르다.

fork() 시스템 호출

2) exec()

  exex() 시스템 호출은 기존의 프로세스를 새로운 프로세스로 재사용 하는 함수이다. 즉 fork() 시스템 호출이 새로운 프로세스를 복사하는 것이라면 exec() 시스템 호출은 프로세스는 그대로 둔 채 내용만 바꾼다.  exec() 시스템 호출을 하면 코드 영역의 기존 내용을 지우고 새로운 코드로 바꾸며, 데이터 영역 역시 새로운 변수로 채워지고 새로운 코드로 바뀐다. 프로세스 제어 블록의 경우 프로세스 구분자나 PPID, CPID, 메모리 관련 사항 등은 변하지 않지만 기존의 프로세스와 다른 새로운 내용을 수행하는 프로세스가 될 것이므로 프로그램 카운터 레지스터 값을 비롯한 각종 레지스터와 사용한 파일 정보들이 리셋된다.

 exec() 시스템 호출을 사용하는 이유는 프로세스의 구조체를 재활용하기 위함이며, 이 경우 프로세스 제어 블록과 메모리 영역, 부모-자식 관계를 그대로 사용할 수 있어 편리하다.

exec() 시스템 호출

 

4. 스레드의 개념

 스레드는 프로세스의 코드에 정의된 절차에 따라 CPU에 작업 요청을 하는 실행 단위이다. (운영체제 입장에서의 작업 단위는 프로세스, CPU 입장에서의 작업 단위는 스레드)

 멀티스레드는 프로세스 내 작업을 여러 개의 스레드로 분할하여 작업의 부담을 줄이는 프로세스 기법인데, 이는 멀티태스킹(시분할 시스템)의 단점을 보완한다.

 

4. 멀티 스레드와 멀티 태스킹

 fork() 시스템 호출로 프로세스를 복사하면 불필요한 정적 영역이 중복되어 생성된다. 이 문제를 해결하기 위해 하나의 프로세스 내에 여러 개의 스레드를 생성하는 것이 멀티 스레드이다. 멀티 스레드는 코드, 파일 등의 자원을 공유하여 자원의 낭비를 막는다. 즉 두 개의 프로세스를 만드는 대신 코드와 데이터 등을 공유하며 여러 개의 일을 하나의 프로세스에서 수행하는 것이다.

멀티 태스킹과 멀티 스레드의 비교

 멀티 스레드를 사용할 경우 한 스레드가 입출력으로 작업이 진행되지 않더라도 다른 스레드가 작업을 계속해서 수행하므로 작업 요구에 빨리 응답할 수 있으며, 위에서 언급했듯 불필요한 자원 낭비를 막아 시스템 효율을 높인다. 한편 멀티 스레드는 다중 CPU에 적합한데, 다중 CPU가 멀티 스레드를 동시에 처리하여 프로세스 처리 시간을 단축할 수 있기 때문이다.

1. 운영체제의 정의

 운영체제란 응용 프로그램이나 사용자에게 컴퓨터 자원을 사용할 수 있는 인터페이스를 제공하고 그 결과를 돌려주는 시스템 소프트웨어로, 응용 프로그램이나 사용자에게 모든 컴퓨터 자원을 숨기고 정해진 방법으로만 컴퓨터 자원을 사용할 수 있도록 제한한다. 운영체제는 하드워에어 대한 책임을 맡으며, 소프트웨어에 하드웨어 자원들을 적절히 분배한다. 따라서 응용프로그램과 사용자는 운영체제를 거치지 않고서는 하드웨어에 접근할 수 없다.

 PC 운영체제에는 윈도우, MAC OS, 유닉스, 리눅스 등이 있으며 모바일 운영체제에는 IOS와 안드로이드 등이 존재한다.

운영체제의 정의

2. 운영체제의 역할

1) 자원 관리

 운영체제는 컴퓨터 시스템의 자원을 응용 프로그램에 배분하여 사용자가 원활하게 작업을 진행할 수 있도록 돕는다.

2) 자원 보호

 비정상적인 작업으로부터 컴퓨터 자원을 보호한다.

3) 하드웨어 인터페이스 제공

 다양한 하드웨어를 일관된 방법으로 사용할 수 있도록 지원한다. (CPU나 마우스, 키보드 등의 종류가 달라도 같은 방법으로 사용할 수 있도록 지원한다.)

4) 사용자 인터페이스 제공

  사용자가 운영체제를 편리하게 사용할 수 있도록 지원한다. (예컨대 과거에는 키보드만으로 운영체제를 다루어야 했기 때문에 컴퓨터 사용법 습득이 어려웠으나, 현대에는 GUI의 제공으로 대부분의 작업을 마우스로 수행할 수 있다.)

3. 운영체제의 구조

 운영체제는 크게 커널과 인터페이스로 구성된다. 커널은 프로세스,메모리,저장장치 등을 관리하며 운영체제의 핵심 기능을 수행하며, 사용자는 커널에 접근할 수 없다. 인터페이스는 커널에 사용자의 명령을 전달하고 실행 결과를 사용자에게 알려주는 커널과 사용자의 매개체 역할을 한다.

운영체제의 구조

 시스템 호출은 사용자나 응용 프로그램이 컴퓨터 자원에 직접 접근하지 못하도록 막아 자원을 보호한다.

 시스템 호출을 통하여 자원에 접근하게 되면 직접 자원에 접근할 때보다 오작동이나 사용자의 부주의로 인하여 컴퓨터 자원이 망가지는 일을 방지할 수 있다. 따라서 응용 프로그램은 하드웨어 자원에 접근하거나 운영체제가 제공하는 서비스를 이용할 때 시스템 호출을 사용한다.

 이때 시스템 호출은 함수 형태로 제공되는데, 예를 들어 응용 프로그램이 하드디스크에 데이터를 저장해달라고 요청할 땐 write() 함수를 사용하며, 데이터를 읽어올 때는 read() 함수를 사용한다.

 

 드라이버 하드웨어와 드라이버의 인터페이스 역할을 수행한다.

 컴퓨터 하드웨어는 종류가 아주 많은데, 운영체제가 많은 하드웨어를 사용하기 위해서는 각 하드웨어에 맞는 프로그램을 직접 개발해야 한다. 그러나 운영체제가 이를 직접 모두 개발하는 것은 현실적으로 어렵고 하드웨어의 특성은 하드웨어의 개발자가 더 잘 알고 있기 때문에 하드웨어 제작자가 직접 이와 관련된 소프트웨어를 개발하는 것이 일반적이다. 이때 하드웨어 제작자가 직접 개발한 소프트웨어를 디바이스 드라이버라고 한다. (물론 운영체제에서 직접 개발한 드라이버도 존재한다.)

4. 가상머신

 가상머신은 운영체제와 응용프로그램 사이에서 작동하는 프로그램이다. 

 예를 들어 C언어로 만든 코드는 유닉스에서는 잘 작동하지만 C언어와 윈도우의 호환성이 떨어져 이를 윈도우 코드에 맞게 수정해야 한다. 따라서 C언어로 프로그램을 만들게 되면 윈도우용, 유닉스용 두 개를 만들어야 하는 번거로움이 생긴다. 이를 해결한 것이 JAVA 언어인데, JAVA가 이러한 문제를 해결한 원리가 가상머신을 이용한 것이다. 운영체제 위에 가상머신을 만들고 그 위에서 응용 프로그램을 작동하게 한 것이다. 

 

가상머신의 사용 예시

 위 그림처럼 운영체제가 응용프로그램 B와는 호환이 잘 되지만 응용프로그램 A와 호환성이 떨어진다면 응용프로그램 B는 가상머신 없이 운영체제 위에서 잘 작동하지만, 응용프로그램 A를 원활하게 시행하기 위해서는 응용프로그램 A와 운영체제를 연결하는 가상머신이 필요할 것이고, 이 가상머신 위에서 응용프로그램 A가 작동할 것이다.

+ Recent posts