반응형

Linux의 프로그램, 프로세스, 쓰레드

 

1. 실행환경 

  1.1) Foreground Process : 실행 후 종료시까지 사용자가 다른 입력을 하지 못하는 프로세스

      * Foreground Process는 프로세스 실행 도중에 사용자의 입력을 받지 않기 때문에 입력을 넣기 위해서는 기존 실행중인 프로세스를 중지시켜야 하고, 이 명령은 [Ctrl + Z]로 가능하다. 이렇게 중지시킨 프로세스는 jobs 명령어를 통해 확인이 가능하고, bg 명령어를 통하여 재시작할 수 있다.

 

  1.2) Background Process : 사용자 입력과 상관없이 실행되는 프로세스

      * ex) find / -name '*.py' > list.txt & 

             와 같이 맨 뒤에 &를 붙이면 Background Process로 실행된다.

 

2. 프로세스

 : 메모리에 적재되어 실행중인 프로그램

1) 프로세스의 생성

* 프로세스 ID : 파일의 inode처럼 각 프로세스는 해당 시점에 unique한 값을 가진다. 최초의 프로세스는 init으로 프로세스 id가 1이며 운영체제에 의해 생성된다. 프로세스 id의 최대값은 32768(2의 15승, 16비트)이다.

1) 기존 프로세스에서 fork() 시스템 콜을 호출하면, 기존 프로세스를 복사하여 새로운 프로세스 공간을 만든다. 

(부모 프로세스 : 기존 프로세스, 자식 프로세스 : 기존 프로세스를 복사하여 생성된 프로세스)
2) fork() 시스템 콜 이후 자식 프로세스 → 부모 프로세스 순으로 코드를 실행한다.

( 자식 프로세스의 pid = 0)
3) 이 때 부모 프로세스가 먼저 종료되는 일이 없도록 wait()함수를 사용한다. 
4) 프로세스가 종료되면 exit()을 통하여 종료 상태가 기록되고, 부모 프로세스에 SIGCHLD 시그널이 보내지며, wait()가 풀리면서 부모 프로세스의 코드가 실행된다. 

 

* 부모 프로세스 : 기존에 존재하는 프로세스

* 자식 프로세스 : fork()를 통해 기존에 존재하는 프로세스를 복제하여 생성되는 프로세스

 

* 프로세스 관련 시스템콜

* getpid() : 프로세스 id를 리턴한다.
* getppid() : 부모 프로세스의 프로세스 id를 리턴한다.
* fork() : 새로운 프로세스 공간을 별도로 만들고, fork()시스템콜을 호출한 프로세스(부모 프로세스)공간을 모두 복사한다.
자식 프로세스는 pid가 0으로 리턴, 부모 프로세스는 실제 pid리턴, 두 프로세스의 변수 및 PC 값은 동일하다. fork 이후의 코드만 두번 실행된다. * exec() 시스템콜 : 시스템콜을 호출한 현재 프로세스 공간의 text, data, bss 영역을 새로운 프로세스의 이미지로 덮어씌움, 시스템 콜 여러가지가 있다.

 

* exec(실행파일이름/인자) : 해당 실행 파일의 데이터를 현재 프로세스에 덮어씌운다. 일반적으로 fork 후에 자식 프로세스에서 exec를 실행하여 부모 프로세스와 다른 프로세스를 만든다. exec는 다양한 종류의 함수가 존재한다.

1) execl("파일이름", "argv[0]", "argv[1]", ...., NULL(맨 마지막은 무조건 null))
2) execlp : 파일 이름이 path에 있으면 path 안써줘도 된다.
3) execle 는 직접 환경변수 지정
4) execvp()
5) execv()
6) execve()

* wait(int *status) : fork()함수 호출 시, 자식 프로세스가 종료될 때까지 부모 프로세스가 기다리게 된다. 이 때, 부모 프로세스가 자식 프로세스보다 먼저 죽는 경우를 막기 위해 사용한다. 자식 프로세스가 종료될 때 exit(int *status) 시스템 콜을 통하여 status 정보가 전달되고, wait()에서 해당 값을 읽고 진행한다. 
(부모 프로세스는 status & 0377 계산값으로 자식 프로세스 종료 상태 확인 가능하다.)

 

* exit() : 시스템 콜, 프로세스를 강제로 즉시 종료시킨다. 인자로 프로세스 종료 상태 번호를 전달한다.  
프로세스 종료 상태 번호 :  
1) 0 : 정상 종료 
2) 1 : 비정상 종료 

주요 동작 과정 :  
1) atexit()에 등록된 함수 실행(등록한 함수를 역순으로 실행한다.) 
2) 열려있는 모든 입출력 스트림 버퍼 삭제 
3) 프로세스가 오픈한 파일을 모두 닫음 
4) tmpfile() : 함수를 통해 생성한 임시 파일 삭제 

 

* nice(int num) : 현재 실행중인 프로세스의 우선순위 변경, 변경할 숫자를 인자로 전달한다. 
* getpriority(int which, id_t who) : 현재 프로세스의 우선순위 조회 
* setpriority(which, id_t who, int num) : 특정 프로세스의 우선순위 변경 

3. 프로세스간 커뮤니케이션 : IPC

1) 파이프 : 프로세스에서 파이프를 만들고 fork()를 하면, 부모가 자식에게 단방향 통신 가능하다.

* pipe(int arr) : 파이프 생성
-> pipe를 생성한 뒤 인자값으로 int형 배열 int arr[2]를 전달한다. 이 때 부모는 arr[1]에다 write하고, 자식은 arr[0]을 읽는다.

2) 메시지 큐

* msgget(key, msgflg) : 메세지 큐 생성
* msgsnd(msqid, &sbuf, buf_length, IPC_NOWAIT) : 메시지 전송
* msgrcv() : 메시지 읽기, 읽는 메시지 타입 지정할 수 있음

* ftok() : 키 생성을 위한 함수

3) 공유메모리 : kernel space에 메모리 공간을 만들고, 해당 공간을 변수처럼 쓰는 방식

* shmget(key_t key, size_t size, int shmflg) : 공유 메모리 생성
* shmat(int shmid, const void *shmaddr, int shmflg) : 공유 메모리 연결
* shmdt(int shmid) : 공유메모리 해제

*ipcs : 리눅스 명렁어, 현재 message queue, shared memory, semaphore 등 생성된 것들을 확인할 수 있다.

4) 시그널 : 커널 또는 프로세스에서 다른 프로세스에 어떤 이벤트가 발생되었는지를 알려주는 전통적인 기법
프로세스는 PCB에 해당 프로세스가 블록 또는 처리해야하는 시그널 정보를 관리한다. 


시그널의 종류 : 
1) SIGKILL : 프로세스를 죽여라
2) SIGALARM : 알람을 발생한다.
3) SIGSTP : 프로세스를 멈춰라(Ctrl + z)
4) SIGCONT : 멈춰진 프로세스를 실행하라
5) SIGINT : 프로세스에 인터럽트를 보내서 프로세스를 죽여라(Ctrl + c)
6) SIGSEGV : 프로세스가 다른 메모리 영역을 침범했다.
(이것 외에도 많다)


* signal(SIGINT, SIG_IGN) : 받은 시그널에 따른 동작 정의
SIG IGN - TLRMSJF ANTL, SIG_DFL - 디폴트 동작


3) 기타

* copy-on-write : fork()는 새로운 프로세스 공간을 생성하고 기존 프로세스를 복사한다. 복사할 때, 4GB나 복사하는 것은 많은 자원의 소모를 야기한다. 따라서 자식 프로세스에서 데이터 사용 시, 부모 프로세스의 페이지를 우선 사용한다.(내용은 동일하기 때문) 읽기에 대해서는 부모 페이지를 계속 참조하고, 쓰기를 할 때 부모 페이지를 복사하고 분리한다. 
(필요한 페이지만 새로 생성하여 사용)

 

ex) 리눅스의 프로세스에 할당된 4GB의 가상 메모리 중 1GB는 커널 메모리인데, 해당 메모리는 각 프로세스마다 모두 공유한다.(물리적으로 동일) 

 

* Pthread : 유닉스 시스템 핵심 스레딩 라이브러리 

pthread.h에 정의되어 있으며, 모든 함수는 pthread_로 시작한다.

1) 스레드 관리 

1. pthread_create() 
2. pthread_exit() 
3. pthread_join() : 특정 쓰레드가 끝나는 시점을 결정, 해당 함수 아래 코드는 해당 함수에 인자로 전달된 쓰레드가 종료될때까지 실행되지 않는다.(이 때 쓰레드 종료 시 상태값을 전달하며, 리소스 해제) 
4. pthread_detach() : 특정 쓰레드가 종료되면 즉시 관련 리소스를 해제한다. 종료 후 추가 처리 없음 

2) 동기화 : 임계영역 관리를 위해서 mutex와 같은 방법을 사용해야 한다. 

1. pthread_mutex_lock() 
2. pthread_mutex_unlock() 

* 파일시스템 

1) 동적 메모리 생성 : 
  1.1) malloc : heap 영역에 동적 메모리 할당 
  1.2) free : 메모리 해제 
  1.3) mmap : 파일의 특정 공간을 메모리의 특정 공간에 mapping해둔다. 따라서 시간,자원이 많이 걸리는 프로세스와 파일간의 interaction을 최소화한다.(메모리의 주소값을 리턴) 
  1.4) munmap : mapping된 물리 메모리 주소를 해제한다. 
  1.5) msync : 메모리와 파일의 데이터를 동기화 

2) inode 
  2.1) stat() : inode 상태값들을 가져온다. 

반응형
반응형

Linux의 Shell 사용법

* Shell : 사용자와 컴퓨터 하드웨어 또는 운영체제 간 인터페이스, bash라는 이름의 Bourne-Again Shell이 리눅스에서 기본적으로 사용된다.(사용하는 Shell의 종류에 따라 문법이 조금씩 달라질 수 있다.)

 

*쉘에서 파일 실행 시 ./를 앞에 반드시 붙여야 하는데, 이는 현재 디렉토리가 일반적으로 기본 path에 설정되어 있지 않기 때문이다. 

 

 

 

* 쉘 스크립트


서버작업 자동화 및 운영(DevOps)를 위해 기본적인 쉘 문법을 익힐 필요가 있다.
파일의 가장 위의 첫 라인은 #!/bin/bash 로 시작하며, 일반적으로 [파일이름.sh]로 저장한다. 

1) 주석 : #

 

2) 변수선언
변수명=데이터 (띄어쓰기를 쓰지 말아야 한다.)

 

3) 출력
echo $변수이름

 

4) 리스트 변수
daemons=("httpd" "mysqld" "vsftpd")

echo ${daemons[1]} → 배열의 두번째 인덱스 출력
echo $daemons[@]} → 배열의 모든 데이터 출력
echo ${daemons[*]} → 배열의 모든 데이터 출력
echo ${#daemons[@]} → 배열의 크기 출력

filelist = ( $(ls) ) → 배열의 ls 실행 결과를 배열로 받음
echo ${filelist[*]} → 배열의 모든 데이터 출력

5) 사전에 정의된 지역변수

$$ : 쉘의 프로세스 번호
$0 : 쉘스크립트 이름
$1 ~ $9 : 명령줄 인수(명령줄로 전달받는 인자)
$* : 모든 명령줄 인수리스트
$# : 인수의 개수
$? : 최근 실행한 명령어의 종료값
 - 0(성공), 나머지 에러

6) 연산자
expr : 숫자 계산
expr을 사용하는 경우 모두 띄어쓰기를 해야 하며 따옴표는 `를 사용해야 한다. 
연산자 *와 괄호() 앞에는 역슬래시()와 같이 사용

ex) num='expr \( 3 \* 5 \) / 4 + 7`

7) 조건문

*if문
if [ 조건 ]
then
명령문
else
명령문
fi


*for 문 
for 변수 in 변수값1 변수값2 ... 
do 
명령문 
done 

*while 문 

while [ 조건문 ] 
do 
명령문 
done 


*조건
-z 문자 : 문자가 null이면 참
-n 문자 : 문자가 null이 아니면 참
값1 -eq 값2 : 값이 같음
값1 -ne 값2 : 값이 같지 않음
값1 -lt 값2 : 1이 2보다 작음
값1 -le 값2 : 1이 2보다 작거나 같음
값1 -gt 값2
값1 -ge 값2

*파일 검사
 -e 파일명 : 파일이 존재하면 참
-d 파일명 : 파일이 디렉토리면 참
이것 등등 참고하기

* 논리연산
똑같음 &&, ||, !, true, false

 

* 기타
*tar : 일반 shell 명령어, 묶고 압축 
option :
1) x - 묶음 해제
2) c 파일 묶음
z : gunzip을 사용
f : 파일 이름을 지정
tar -cvzf 압축파일이름 압축파일명
tar -xvzf 압축 해제할 압축 아카이브 이름

*find : 검색
ex) find . -type -f -name '파일명검색어 -exec bash -c "명령어1; 명령어2;" \;
ex) find . -type f -name '*log.?' -mtime +GZIPDAY -exec bash -c "gzip {}" \;
파일명 검색한 다음 명령어를 실행해라. 

반응형
반응형

Linux!

 

많은 서버의 os가 리눅스 기반이기 때문에 대부분의 개발자들이 거의 필수적으로 사용하는 운영체제입니다.

앞에서 배웠던 운영체제 이론을 기반으로, 리눅스에서는 운영체제가 어떻게 구현되어있는지 알아보겠습니다.

 

1. 리눅스의 역사

 

1) 리누스 토발즈가 개발

2) 유닉스(UNIX)와 유사한 운영체제

3) GNU 프로젝트 산출물리눅스 커널이 통합됨

 

2. 리눅스의 특징

1) 모든 시스템을 파일 시스템처럼 다룸

2) 서버, 클라우딩 컴퓨팅에 많이 사용됨(무료)

3) C(ANSI C)언어를 기반으로 제작됨

4) UNIX 계열의 OS(다중사용자, 다중 작업 지원)

 

3. 리눅스와 파일 시스템

 

1) 리눅스의 파일 탐색

  리눅스는 A드라이브, C드라이브 같은 네임스페이스가 없고 전역 네임스페이스를 사용한다. 모든 디렉토리는 디렉토리 엔트리(dentry)라는 구조체를 포함하며, 디렉토리 엔트리에는 파일, Sub 디렉토리에 대한 inode 정보들이 포함되어 있다.

 

2) 하드링크와 소프트링크

  리눅스에서 파일을 복사하거나 링크를 거는 방법에는 세 가지가 있다.

  2.1) 복사(cp) :

    파일을 복사한다. 원본 파일이 2개가 된다.

  2.2) 하드 링크 : 

    파일 원본은 하나이나, 원본 inode 구조체를 가리키는 링크를 추가적으로 만든다. 즉, 동일한 Inode이나 파일 이름만 다른 구조를 갖는다. 원본 파일을 지운다고 해도 하드링크에는 영향을 미치지 않는다.

  2.3) 소프트링크 : 

    파일 원본은 하나이고, 새로운 Inode 구조체를 만들어 가리킨다. 새로운 Inode의 구조체가 다시 원본 파일을 가리키데 된다. 따라서, 원본 파일을 삭제하면 접근이 불가능하다.(Windows의 바로가기와 동일)

4. 리눅스의 시스템 콜, C라이브러리

* 시스템 콜 : 운영체제 리소스나 서비스 요청을 위해, 사용자 영역에서 커널 영역으로 들어가는 함수, 리눅스가 C언어로 작성되어 있으므로, 시스템 콜도 C언어로 작성되어 있다.

 

1) 시스템 콜의 구현

→ 시스템 콜이 호출되면, 소프트웨어 인터럽트 명령을 호출하면서 0x80을 넘겨준다. 

eax 레지스터에는 시스템 콜 번호를, ebx 레지스터에는 시스템 콜에 해당하는 인자값을 전달한다.
→ CPU가 사용자 모드를 커널 모드로 바꿔준다.
→ Interrupt Descriptor Table에서 0x80에 해당하는 주소 함수를 찾아서 실행한다.
→ system_call() 함수에서 eax에 저장되어있는 번호에 해당하는 시스템 콜 함수로 이동한다.
→ 해당 함수 실행 후, 사용자 모드로 변경한다.

 

2) C 라이브러리 
- 유닉스 : libc
- 리눅스 : GNU libc

 

* C 컴파일러

 

* Shell에서 C파일을 실행시키는 법 :

1) C컴파일러로 파일을 컴파일한다.

    (gcc -o [파일명] [실행파일명])

2) 1)번의 결과로 생성된 실행파일을 실행한다.

 

* ABI : Application Binary Interface
→ 함수 실행 방식, 레지스터 활용, 시스템 콜 실행, 라이브러리 링크 방식 등을 정의

 

반응형
반응형

파일 시스템 : 운영매체가 저장매체에 파일을 쓰기 위한 자료구조 또는 알고리즘

 

파일 시스템은 위와 같이 정의됩니다. 데이터들을 관리하기 위해 만들어진 시스템이죠.

저장된 bit 단위의 데이터들이 모여 4kb(일반적)의 Block을 구성하며, Block들이 모여 파일을 구성합니다.

 

* 파일 저장 방법 

 : 물리적 공간이 허용하는 경우 연속적으로 데이터를 저장하는 것이 이상적이나, 데이터 크기/공간적인 제약때문에 불연속적인 공간에 저장해야 하는 경우가 많습니다. 따라서 아래와 같은 저장 방법을 활용합니다.

1. 블록 체인 : 블록을 링크드 리스트로 연결

2. 인덱스 블록 : 블록마다 위치 정보를 기록

 

* 운영체제별 파일시스템

1. Windows : FAT, FAT32, NTFS

  - 블록 위치를 FAT라는 자료구조에 기록하는 방식

2. Linux(UNIX) : ext2, ext3, ext4

  - 인덱스 블록 기법인 inode 방식을 사용

 

Linux의 파일시스템인 inode에 대하여 자세히 알아보겠습니다.

일반적인 파일시스템에서는 블록으로 이루어진 파일이라는 단위로 데이터를 관리합니다. 

 

* inode 파일 시스템 구조 : 

1. 수퍼 블록 : 파일 시스템 정보 및 파티션 정보

2. inode 블록 : 파일에 관한 상세 정보 (Mode, Owner Info, Size ...)

  * inode에는 데이터 block들을 가리키는 다양한 구조가 있다. 바로 데이터를 가리키는 Direct Block도 있고, Direct Block들을 모아놓은 Single indirect 구조 등도 포함하고 있다.

3. 데이터 블록 : 실제 데이터

 

* 리눅스는 '모든 것은 파일'이라는 철학을 가지고 운영체제 상에서 일어나는 모든 interaction을 파일 시스템같이 구성해 놓았습니다. 따라서 모든 interaction은 파일을 읽고 쓰는 것처럼 이루어집니다.

ex) I/O device(마우스, 키보드) 기술도 파일을 읽고 쓰는 것같이 다루어진다.

반응형
반응형

  지난번까지 배웠던 프로세스에 관해 생각해보면, 하나의 프로세스는 4GB의 메모리공간을 가진다고 했습니다. (4GB의 공간이 정해진 이유는, 32bit 시스템에서 2의 32승이 4GB이기 때문입니다. 한번에 전달 가능한 주소값의 범위가 0~4GB라는 뜻입니다.)

그렇다면 일반적으로 4GB ~ 32GB의 범위를 갖는 주기억장치 RAM에는 동시에 최대 8개의 프로세스밖에 실행될 수 없는 것일까요? 단순히 작업 관리자만 켜보아도 수십,수백개의 프로세스가 실행되고 있는 것을 보아 그건 아닌 것을 알 수 있습니다. 이런 마법을 가능하게 하는 것이 가상 메모리, 페이징 시스템입니다.

 

1. 가상 메모리

  프로세스의 PCB인 4GB는 사실 모두 메모리(RAM)에 올라가는 것이 아닙니다. 당장 사용될 예정인 데이터만 메모리에 올라가게 되죠. 하지만 PCB는 0~4GB의 주소값을 가지고 있습니다. 이 주소값들은 어딜 가리키고 있는 걸까요?

  PCB에 할당된 4GB의 주소는 '가상'의 주소값입니다. 실제 물리 주소값이 아니라, 가상으로 주어진 주소값이라는 얘기죠. 우리가 이 가상 주소값을 호출하면, MMU(Memory Management Unit)이라는 장치에서 가상 주소값을 물리 주소값으로 변환해줍니다. 그럼 CPU에서 실제 물리값에 존재하는 데이터를 사용하여 프로세스를 실행하는 것이죠. 이런 방식으로 수십,수백개의 프로세스에 필요한 데이터를 메모리(RAM)에 올릴 수 있는 것입니다. 이렇게 메모리에 올려지는 데이터를 관리하는 시스템이 '페이징 시스템'입니다.

2. 페이징 시스템

  크기가 동일한 페이지라는 단위로 가상 주소와, 이에 매칭되는 물리 주소 공간을 관리하는 방식을 페이징 시스템이라고 합니다. 프로세스의 PCB는 4GB의 주소를 가지며, 4GB의 공간은 페이지라는 단위로 나뉘어져 관리됩니다. 페이지는 일반적으로 4KB의 크기를 가지며, intel x86시스템에서는 4KB, 2MB, 1GB의 페이지 단위 설정이 가능합니다. (리눅스는 4KB 고정이며, 4GB / 4KB 만큼의 페이지가 존재합니다.)

  아래 그림과 같이 프로세스 A의 PCB는 여러 개의 페이지로 나누어지며, 페이지 테이블을 통하여 메모리의 실제 물리 공간에 있는 페이지 프레임과 매칭됩니다.(페이지와 페이지 프레임은 같은 개념이며 가상 메모리 상의 공간단위를 페이지, 실제 물리 메모리 상의 공간을 페이지 프레임이라고 합니다.) 페이지 테이블은 메모리에 올려져 있으며, 페이지 테이블의 시작 주소가 PCB에 저장되어 있습니다.

 

* 페이지 : 가상 메모리 공간(PCB)를 동일 크기로 나눈 것

* 페이지 테이블 : Dictionary와 같이 '가상 주소값' : '실제 주소값'의 매칭 정보 보관

* 페이지 프레임 : 실제 메모리 공간을 동일 크기로 나눈 것

  페이지를 사용하여 데이터 접근 시, 아래와 같이 페이지 번호와 Offset값을 전달하면 정확한 데이터의 위치에 접근할 수 있습니다.

* 다중 단계 페이징 시스템 : 위와 같이 명령어의 일부분을 페이지 번호(p), 변위(d)로 나타내게 되면 32bit 시스템에서는 모든 페이지를 나타내기 위해서 최소 20개의 bit(100만개의 페이지)가 필요하고, 나머지 12개의 bit는 변위를 나타내게 됩니다. 이를 효율적으로 활용하기 위해서, 사용하지 않는 페이지는 주소값을 생성하지 않고, 필요한 페이지의 주소정보만 생성하여 관리하는 시스템을 다중 단계 페이징 시스템이라고 합니다. 다중 단계 페이징 시스템에서는 페이지 [디렉토리 - 페이지 테이블 - 변위] 라는 계층적 구조를 이루고 있습니다. 

  

지금까지 학습한 내용을 정리해보면,

  1. CPU에서 프로세스 A에서 필요한 데이터의 가상 주소를 MMU에 전달

  2. MMU에서 메모리에 올려져 있는 페이지 테이블에 접근하여 전달받은 가상 주소와 매칭되는 물리 주소값을 찾아냄

  3. 메모리에서 Data를 CPU로 전달

과 같은 과정을 거치게 됩니다.

 

 

Computer Architecture에서도 배웠지만, 일반적으로 속도가 빠른 메모리일수록 비싸고, 중요한 곳에 사용됩니다. 위와 같이 CPU에서 요청한 데이터를 불러오는데 MMU, Memory같이 상대적으로 속도가 느린 메모리를 참조하게 되면 CPU 입장에서는 엄청난 시간 Loss가 발생하게 되죠.

 

  이같은 Loss를 방지하기 위하여 시스템에는 TLB(Translation Lookaside Buffer)라는 메모리가 존재합니다. 해당 메모리에는 최근 사용한(혹은 사용할 가능성이 높은) '가상 메모리'에 매칭되는 실제 물리 주소값이 기록되어 있어, TLB에 존재하는 데이터에 한해서는 MMU가 주기억장치의 페이지 테이블을 참조하지 않아도 됩니다. (속도 ↑) 

  MMU가 CPU로부터 가상 메모리를 전달받으면, 우선 TLB에 매칭되는 물리 주소값이 있는지 확인한 뒤 없으면 페이지 테이블을 참조하게 됩니다.

 

* 요구 페이징 : 프로세스가 실행되기 위해서는 프로그램이 메모리에 올라가야 하는데, 이 때 모든 데이터를 주기억장치(RAM)에 올리는 것이 아니라 실행 시점에 필요한 데이터만 메모리에 올리며, 더이상 필요하지 않은 페이지 프레임은 다시 저장매체(HDD, SSD)로 이동시킵니다. 이같은 방식을 요구 페이징이라고 부릅니다.

  이와 같이 필요하다고 생각되는 데이터만 메모리에 올리는 과정에서, CPU가 요청한 데이터가 메모리에 올라가져있지 않은 상황이 발생할 수 있습니다. 이같은 상황이 일어나면 페이지 폴트(Page Fault)라는 인터럽트가 발생하며, 해당 인터럽트를 받은 OS는 저장매체에서 메모리로 필요한 페이지를 이동시키게 됩니다. 페이지 폴트가 많이 일어나면 실행 시간에 많은 Loss가 생깁니다.

 

  이제 정말 마지막으로 CPU에서 가상 메모리를 바탕으로 프로세스 실행에 필요한 데이터에 접근하는 과정을 살펴보겠습니다.

 

  1. CPU에서 가상 주소값을 MMU로 전달

  2. MMU에서 전달받은 가상 주소값을 TLB에서 확인

  3. 있으면 바로 매칭되는 물리 주소값에 접근, 없으면 메모리의 페이지 테이블에서 매칭되는 값을 찾은 후 해당 물리 주소값에 접근

  (이 때 요청된 데이터가 메모리에 존재하지 않아 페이지 테이블에 없다면, 페이지 폴트가 발생하며 저장매체에서 해당 데이터가 메모리로 로드된다. 데이터가 로드된 이후에는 페이지 테이블이 업데이트되며, 가상 주소값을 다시 전달받아 1번부터 다시 수행한다.)

  4. 물리 주소값에 존재하는 데이터를 CPU로 전달

 

이상 가상 메모리와 페이징 시스템에 대해서 알아보았습니다. 끝-!

 

* 세그멘테이션 기법 : 페이징 시스템에서는 동일한 크기를 가지는 페이지의 단위로 물리 메모리가 나뉘어졌다면, 세그멘테이션 기법에서는 이 단위가 유동적입니다. 페이징 시스템에 비하여 유동적으로 메모리를 관리할 수 있다는 점이 좋지만, 그만큼 복잡하기도 하고 하드웨어적인 지원이 되어야 합니다.

반응형
반응형

  이번에는 메모리에 올라간 프로세스들을 어떻게 실행하는지 알아보겠습니다. 언제, 어떤 프로세스를 실행할지를 결정하는 OS의 프로그램을 스케쥴러라고 합니다. 스케쥴러는 CPU를 비롯한 컴퓨터 자원들을 효율적으로 분배하는 역할을 하며, 일반적으로 다음과 같은 특징을 가지고 있습니다.

[스케쥴러의 특징]

 

1) 시분할 시스템 : CPU 사용시간을 잘게 쪼개어 나누어 사용하는 방법. CPU는 짧은 시간동안 돌아가면서 다양한 작업을 수행합니다.

2) 멀티 태스킹 : 프로세스를 잘게 쪼개어 수행하는 방법. CPU는 기본적으로 한번에 하나의 작업만을 수행할 수 있습니다.(동시에 여러가지 작업 수행 불가) 하지만 프로세스를 여러 개의 작업으로 잘게 쪼개어 수행하면, 여러 프로세스들이 동시에 실행되는 것과 같이 사용자가 느끼게 할 수 있습니다. 

3) 멀티 프로세싱 : 여러 개의 CPU에 잘게 쪼개진 프로세스를 병렬로 실행하여 실행속도를 극대화하는 방법.

4) 멀티 프로그래밍 : CPU의 효율적인 사용을 위하여, 프로세스 수행 과정에서 CPU를 활용하지 않을 때, 다른 프로세스를 수행하는 방법. 

 

ex) 특정 프로세스가 수행되던 도중 디스크에서 파일을 읽는 작업을 수행한다면, 디스크에서 파일을 읽어오는 동안 CPU는 놀게 됩니다. 이 시간을 효율적으로 사용하기 위하여, 기존 프로세스를 Block하고 다른 프로세스를 수행합니다. 이 Block된 프로세스는 파일 작업이 완료되었다는 신호를 받으면 다시 수행될 수 있습니다.

 

* DMA(Direct Memory Access) : 프로세스 작업 도중 디스크에 있는 데이터가 필요한 경우, CPU가 직접 데이터를 가져오는 것은 비효율적이기 때문에 만들어진 데이터를 디스크에서 읽어오는 별도의 작업을 하는 시스템

 

[스케쥴러의 종류]

 

1. 시간

  1) RTOS(Real Time OS) : 응용프로그램의 실시간 성능 보장을 목표로 하는 OS(정확한 시작/완료 시점을 보장) 

  2) GPOS(General Purpose OS) : (프로세스 실행시간에 민감하지 않은) 일반 용도의 OS

 

2. 방식

  1) FIFO : 들어온 순서대로 프로세스를 실행(배치 처리)

  2) SJF(Shortest Job First) : 작업시간이 짧은 순서대로 프로세스를 실행(작업시간 예측이 어렵다.)

  3) 우선순위 기반(Priority Based) : 프로세서에 할당된 우선순위가 높은 것부터 순차적으로 실행

     * 정적 우선순위 할당 : 미리 프로세스마다 우선순위를 지정해놓음

     * 동적 우선순위 할당 : 스케쥴러가 상황에 따라 우선순위를 동적으로 지정

  4) Round-Robin : 시분할 시스템에 따라 프로세스들은 정해진 시간씩 수행(A 프로세스 3초 - B 프로세스 3초 ...)

     * Time Quantum : Round-Robin 스케쥴러의 프로세스 실행 시간 단위, Time Quantum 이 3초라면 위와 같이 3초마다 프로세스를 바꿔가면서 실행합니다. 프로세스를 바꾸는 과정을 Context Switching이라고 하는데, Time Quantum이 짧아질수록 Context Switching이 자주 일어나 CPU에 부담이 될 수 있습니다.

 

3. 선점 여부

 

  1) 선점형 스케쥴러(Preemptive Scheduling) : 하나의 프로세스가 기존에 CPU를 선점하고 있는 프로세스를 중단시키고 실행할 수 있는 방식.

  2) 비선점형 스케쥴러(Non-Preemptive Scheduling) : 하나의 프로세스가 끝나지 않으면 다른 프로세스는 CPU를 사용할 수 없는 방식. 

 

[스케쥴러의 실행]

 일반적인 OS의 스케쥴러는 [Preemptive + Priority Based + Round-Robin] 방식을 사용합니다. 프로세스들을 여러 State으로 관리하며, 프로세스의 I/O Device를 비롯한 다양한 컴퓨터 자원 활용을 관리합니다. 

 

1. 쓰레드(Thread) : 

  프로세스의 Subset(Light Weight Process). 여러 개의 쓰레드가 하나의 프로세스를 이루며, 일반적으로 작업단위로 쓰레드를 구성합니다. 쓰레드들은 멀티 프로세싱을 통하여 여러개의 CPU에서 동시에 실행될 수 있습니다. 하나의 프로세스를 구성하는 쓰레드들은 프로세스의 PCB 중 Heap, BSS, Data, Text 영역을 공유합니다.(Stack은 쓰레드마다 별도 영역 생성) 

 

* 쓰레드의 장점

  1) 자원 공유

  2) 응답성 향상

  3) 코드 간결

  4) CPU 활용도 ↑

 

* 쓰레드의 단점

  1) 하나의 쓰레드에 문제가 있으면, 전체 프로세스가 영향을 받음(자원공유)

  2) 쓰레드가 많으면 Context Switching이 많이 일어남

 

2. 프로세스의 상태 : 

  스케쥴러는 프로세스를 여러 State으로 관리하며, 다음과 같은 State들 중 하나에 프로세스는 존재합니다.

[Process State Diagram]

  1) new : 프로세스가 생성되는 상태

  2) ready : 프로세스가 실행되길 기다리는 상태

  3) waiting : 프로세스가 특정 작업이 끝나길 기다리는 상태(ex : I/O device 처리)

  4) running : 프로세스가 실행되고 있는 상태

  5) terminated : 프로세스가 종료된 상태

 

프로세스가 생성될 때 [new] state에 들어가게 되고 생성이 완료된 프로세스는 [ready] state로 전환됩니다. 자신이 수행될 차례(Scheduler의 우선순위/Round-Robin 방식에 의해 결정)가 되면 [running] state이 되고, I/O Device 관련 처리 혹은 특정 event가 발생할 때, CPU를 다른프로세스에게 넘겨주고 [waiting] state로 전환됩니다. [waiting] state에 있다가 특정 event가 완료되면 프로세스는 다시 [ready] state으로 전환되고, [running] state로 전환되어 프로세스의 수행이 완료되면 [terminated] state로 전환됩니다.

 

3. IPC(Inter Process Communication) : 

  프로세스가 실행되다 보면, 프로세스 간의 데이터를 공유하거나 상호간에 신호를 보내야하는 경우가 있습니다. 이럴 때 사용하는 프로세스간 통신 기법을 IPC라고 하며, 다음과 같은 여러가지 방식이 존재합니다.

 

  1) Message Queue : Message Queue가 존재하고, Key를 바탕으로 메시지를 전달하는 기법

  2) Shared Memory : Kernal Space에 메모리 공간을 만들고, 해당 공간에 변수를 생성하여 데이터를 공유하는 기법

  3) Pipe : 부모 프로세스에서 자식 프로세스로 데이터를 보낼 수 있는 구조(파이프)를 활용하는 기법(부모,자식 프로세스에 대해서는 리눅스 장에서 공부)

  4) Signal : 커널 또는 프로세스에서 다른 프로세스에 특정 Event 발생 여부를 알려주는 기법

  5) Semaphore : 특정 영역에 대해 접근할 수 있는 쓰레드 개수를 한정하여 메모리를 공유하는 기법(IPC만을 위하여 개발된 기법은 아님.)

  6) Socket : 컴퓨터 간 네트워크 통신을 위해 개발된 기법(IPC만을 위하여 개발된 기법은 아님.)

 

* Dead Lock(교착상태) : 두 개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다려서 다음 단계로 진행하지 못하는 상태
* Starvation(기아상태) : 특정 프로세스의 우선순위가 낮아서 원하는 자원을 계속 할당 받지 못하는 상태(부족한 자원 하에) => 우선순위 변경으로 해결(오래 기다린 프로그램 우선순위 높여주는 등)

 

4. 인터럽트(Interrupt) : CPU가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 또는 예외 상황이 발생하여 처리가 필요할 경우에 CPU에 알려서 처리하는 기술

  ex) CPU가 프로세스를 실행하는 도중, 사용자가 마우스(Input Device)를 움직이는 Event가 발생했다고 생각해봅시다. 사용자 입장에서는 마우스를 움직였는데 실행중인 프로세스가 종료될 때까지 마우스가 움직이지 않으면 매우 당황스럽죠. 이런 I/O Device의 입력이 들어왔음을 CPU에 알려주는 기능을 Interrupt라고 합니다.(이 외에도 다양한 용도로 사용됩니다.) 이 기능이 없다면, I/O Device Event가 발생했음을 알기 위하여 매번 I/O Device의 상태를 확인해야 할 것입니다.

 

5. Context Switching : 스케쥴러에 의해 실행중인 프로세스가 중단되고 다른 프로세스가 실행되는 전반적인 과정
  단순한 과정으로 생각할 수 있지만, 자세히 생각해보면 새로운 Process의 PCB를 메모리에 올리고, 기존 프로세스가 실행된 위치를 저장하고 종료해야 하며, PC(Program Counter)를 새로운 위치로 이동시켜야 합니다. 컨텍스트 스위칭이 일어나면 Program Counter(다음 명령어 실행할 곳), Stack Pointer(스택 내에 다음 저장 공간)값을 PCB(Process Control Block)에 저장하고 다른 프로세스를 시작하게 됩니다. 

반응형
반응형

이전 시간에는 컴퓨터의 구성요소에 대해 간단히 알아보았습니다.

이번에는 컴퓨터가 어떻게 프로그램을 실행하고 컴퓨터의 구성요소들을 관리하는지 알아보겠습니다.

 

프로그램 : 컴퓨터를 실행시키기 위한 일련의 순차적으로 작성된 명령어 모음

프로세스 : 컴퓨터에서 실행중인 프로그램(일, task)

 

  프로그램은 우리가 특정 작업을 수행하기 위하여 작성한 코드이며, 이것이 실행될 때 프로세스라고 부릅니다. OS는 다양한 프로세스들이 요청한 작업을 컴퓨터 자원을 활용하여 수행합니다. 그럼 이제 프로그램이 '실행'된다는 것의 의미를 알아보겠습니다.

 

프로세스는 일반적으로 아래와 같은 구조를 가지고 있습니다. 총 4Gb의 메모리로 구성되어 있으며, 0~3Gb까지의 주소를 User Space, 3Gb~4Gb까지의 주소를 Kernal Space라고 부릅니다. 우리가 작성한 프로그램에 관한 정보는 0~3Gb의 User Space의 메모리에 올라가며, 순차적으로 코드가 수행됩니다. 

 

  이렇게 만들어진 프로세스의 메모리 공간을 PCB(Process Control Block)이라고 합니다. PCB는 User Space(0~3Gb), Kernal Space(3Gb ~ 4Gb)로 구성되어 있으며, Kernal Space는 모든 프로세스에서 공유하게 됩니다.(이 부분은 나중에 더 자세히 설명) 

  User Space는 Stack, Heap, Data, Text로 구성되어 있으며 다음과 같은 데이터가 저장됩니다.

 

1. Stack : 프로그램 실행 과정에서 생성되는 지역변수, 주소값 등을 저장하는 공간(순차적으로 주소가 낮아지는 방향으로 쌓임)

2. Heap : 동적으로 할당되는 메모리 공간 (By malloc ...)

3. Data : 1) Data : 초기화된 전역변수가 저장된다. 

            2) BSS : 초기화되지 않은 전역변수가 저장된다.

4. Text(Code) : 프로그램 실행 코드

 

반응형
반응형

이번 시간에는 리눅스 환경에서 프로세스들이 어떻게 작동하는지 알아봅니다.

 

프로세스는 구조는 아래와 같습니다. 아래의 구조를 PCB라고 부르며, 프로세스가 생성되면 고유의 pid(Process ID) 및 PCB가 주어집니다. 이 공간에 프로세스 이미지가 업로드되고, 실행하면 프로세스의 작업이 시작됩니다.

이 때 생성되는 pid는 파일에서의 inode와 같이, 프로세스와 1:1로 관리가 됩니다.(이 때 커널 메모리는 모든 프로세스가 공유합니다.)

 

1) STACK : 함수 실행 간 생성되는 데이터를 정적으로 할당

2) HEAP : 함수 실행 간 생성되는 데이터를 동적으로 할당

3) BSS : 초기화되지 않은 변수 값들을 저장

4) DATA : 초기화된 변수 값들을 저장

5) TEXT : 실행프로그램의 코드를 저장

운영체제가 Load되면 최초의 프로세스인 init이 생성되고, 이 프로세스에 pid = 1이 주어집니다. 리눅스에서는 일반적으로 init을 제외한 다른 프로세스를 생성할 때 fork() 시스템 콜을 사용하여 생성하게 됩니다. 

프로세스의 생성 과정은 아래와 같습니다.

 

[프로세스 생성]

1) 부모 프로세스에서 fork()를 수행한다.

2) fork()를 하면 자식 프로세스의 경우 성공적으로 수행되면 새로운 가상메모리(4GB) 공간을 생성한 뒤 return 값으로 0을 주게 되는데(실패하면 -1), return 값이 0으로 확인되면 exec(실행파일)을 수행한다.

3) exec(실행파일)에 의해서 새 프로세스 공간에 실행파일을 덮어씌우고, 이를 처음부터 실행한다.

4) 자식 프로세스 마지막에 exit(종료 상태값)을 사용하여 자식 프로세스를 종료함과 동시에 종료 상태값을 전달한다.

5) 부모 프로세스에서 wait(종료 상태값)을 사용하여 자식 프로세스의 종료 상태값을 확인하고, 남은 부모 프로세스를 수행한다.

 

* copy-on-write : 부모 프로세스에서 자식 프로세스를 fork()할 때 새로운 가상메모리공간 4GB를 복사하여 만들게 되는데, 4GB를 복사하는데 시간이 매우 오래 걸린다. 이를 개선하기 위해 자식 프로세스에서는 처음에 생성된 뒤 부모의 메모리 공간(물리적)을 참조한다. 읽기 과정에서는 해당 참조값이 없지만, 자식 프로세스의 수행 과정에서 메모리에 write할 일이 생기면, 부모 프로세스의 메모리 공간(물리적)에 그대로 쓰는 것이 아니라 변화가 필요한 내용의 페이지만 새로운 공간에 복사하여 write를 진행한다.

 

[ 프로세스 관련 시스템 콜 ]

 

* fork() : 시스템 콜. 해당 시스템 콜이 실행되면, 운영체제는 새로운 프로세스 공간(PCB)를 만들고, fork() 시스템콜을 호출한 프로세스(부모 프로세스) 공간을 모두 복사해서 새로운 프로세스 공간에 붙여넣습니다. 이렇게 새로 만들어진 프로세스에는 새로운 pid가 부여됩니다. 이 때 부포 프로세스의 id를 ppid라고 하고, 해당 변수도 새롭게 만들어진 프로세스에 저장됩니다.

 


ex)

#include <sys/types.h>
#include 
#include 
int main()
{
        pid_t pid;
        printf("Before fork() call\n");
        pid = fork();

        if(pid == 0)
                printf("This is Child process. PID is %d\n", pid);
        else if (pid > 0)
                printf("This is Parent process. PID is %d\n", pid);
        else
                printf("fork() is failed\n");
        return 0;
}


* exec() : 시스템 콜. 해당 시스템 콜을 호출한 프로세스 공간의 [TEXT, DATA, BSS] 영역을 새로운 프로세스(인자로 전달)의 이미지로 덮어씌웁니다. 따라서 인자로 실행파일을 전달해줘야 합니다.

 

exec()는 fork()와 다르게 여러 버전의 함수가 있습니다. 해당 함수들은 인자를 전달하는 방식이 다릅니다.

 

1) execl() 

2) execlp()

3) execle()

4) execv()

5) execvp()

6) execve()

 

* getpid() : pid(Process ID)를 가져옵니다.

* getppid() :ppid(부모의 Process ID)를 가져옵니다.

 

* wait() : 시스템 콜, 부모 프로세스가 자식 프로세스보다 먼저 종료되는 일이 없도록, 자식 프로세스가 종료될 때까지 기다리게 하는 함수입니다. 자식 프로세스가 종료할 때 종료 상태값(정상 종료 : 0)을 저장하면, 해당 상태값을 참조하여 나머지 부모 프로세스를 수행합니다.

 

* exit() : 시스템 콜, 프로세스를 강제로 즉시 종료시킵니다. 인자로 프로세스 종료 상태 번호를 전달합니다.

# 동작 과정

1) atexit()이라는 함수에 등록된 함수를 역순으로 순차적으로 실행한다.

2) 열려있는 모든 입출력 스트림 버퍼를 삭제한다.

3) 프로세스가 오픈한 파일을 모두 닫는다.

4) tmpfile() 함수를 통해 생성한 임시 파일을 삭제한다. 

 

[ Process 관련 명령어 ]

 

* ps -ef : 현재 실행중인 프로세스의 모든 정보를 출력합니다.

반응형

+ Recent posts