'분류 전체보기' 카테고리의 글 목록 (2 Page) :: YJcode

가상 터미널은 마스터와 슬레이브라는, 연결된 가상 디바이스의 쌍이다. 이 디바이스 쌍은 두 디바이스 사이에서 양방향으로 데이터를 전송할 수 있는 IPC 채널을 제공한다.

가상 터미널의 핵심은 슬레이브 디바이스가 터미널처럼 동작하는 인터페이스를 제공함으로써, 터미널 중심 프로그램이 슬레이브 디바이스에 연결해서 마스터 디바이스에 연결된 또 다른 프로그램을 통해 터미널 중심 프로그램을 구동할 수 있다는 점이다. 드라이버 프로그램의 출력은터미널 드라이버가 수행하는 일반적인 입력과정을 거친 뒤에 슬레이브에 연결된 터미널 중심 프로그램에 입력으로 전달된다. 터미널 중심 프로그램이 슬레이브로 출력하는 것은 모두 드라이버 프로그램으로 전달된다. 다시 말해서 드라이버 프로그램이 보통은 사용자가 일반 터미널에서 수행하는 일을 수행하는 것이다.

가상 터미널은 다양한 응용 프로그램에서 활용되는데, 특히 X윈도우 시스템에서 제공되는 터미널 윈도우의 구현, 텔넷과 ssh같은 네트워크 로그인 서비스를 제공하는 응용프로그램에 주로 쓰인다.

 

프로세스와 관련된 시간에는 두 가지가 있다.

 

  • 실제 시간은 어떤 ㄴ표준 시점으로부터 측정한 시간또는 어떤 정해진 시점, 일반적으로 프로세스의 시작부터 측정한 시간이다. 유닉스 시스템에서 달력 시간은 UTC 1970년 1월 1일부터 흐른 초를 기준으로 측정하고, 영국 그리니치를 지나는 경선에 따라 정의된 시간대에 따라 조정된다. 유닉스 시스템의 탄생일과 가까운 이 날짜를 기원이라고 한다.
  • 프로세스 시간은 CPU 시간이라고도 하는데, 프로세스가 시작된 이래 사용한 CPU시간의 총량이다. CPU시간은 다시 커널모드에서 코드를 실행하는데 소비한 시간인 시스템 CPU시간과ㅣ, 사용자 모드에서 코드를 실행하는데 소비한 시간인 사용자 CPU시간으로 나뉜다.

 

time명령은 파이프라인에 들어 있는 프로세스들을 실행하는데 소요된 실제 시간, 시스템 CPU시간, 사용자 CPU시간을 보여준다.

세션은 프로세스 그룹의 묶음이다. 특징으로는 아래 몇가지를 들 수 있다.

 

  • 세션 내 모든 프로세스는 동일한 세션 ID를 가지고 있다.
  • 세션 리더는 세션을 만든 프로세스이고, 그 프로세스 ID가 세션 ID 가 된다.
  • 세션은 주로 작업 제어 쉘에서 쓰인다.
  • 작업 제어 쉘에서 만든 모든 프로세스 그룹은 쉘과 동일한 세션에 속하고, 쉘은 세션 리더가 된다.

 

세션에는 보통 연관된 제어 터미널이 있다. 제어 터미널은 세션 리더 프로세스가 처음 터미널 디바이스를 열 때 설정된다. 대화형 쉘이 만든 세션의 경우, 제어 터미널은 사용자가 로그인한 터미널이다. 터미널은 최대한 세션의 제어 터미널이 될 수 있다.

제어 터미널을 열면 세션 리더가 그 터미널의 제어 프로세스가 된다. 터미널이 끊어지면 제어 프로세스는 SIGHUP 시그널을 받는다.

어느 싲점에서든 세션 내 프로세스 그룹 중 하나는 포그라운드 프로세스 그룹으로서 터미널에서 입력을 받을 수 있고, 터미널로 출력을 보낼 수 있다. 사용자가 제어 터미널에서 인터럽트 문자나 중지문자를 입력하면, 터미널 드라이버는 포그라운드 프로세스 그룹을 종료시키거나 중지시키는 시그널을 보낸다. 세션은 임의 개수의 백그라운드 프로세스 그룹을 가질 수 있는데 명령끝에 & 문자를 붙여 만든다.

작업 제어 쉘은 모든 작업을 나열하고, 작업에게 시그널을 보내고, 작업을 포그라운ㄷ드와 백그라운드로 전환하는 명령을 제공한다.

현대 유닉스 구현에서 각 프로세스는 여러 개의 스레드를 가질 수 있다. 하나의 프로세스에 하나의 스레드가 존재하는 상태를 싱글 스레드, 두 개 이상의 스레드가 존재하는 상태를 멀티 스레드라고 부르는데, 멀티 스레드는 멀티 프로세스와 본질적으로 거의 동일하다. 단 하나 다른 것이 있는데

 

  • 멀티 프로세스는 각각의 독립적인 변수 환경을 가지고 있다. 따라서 프로세스 A 가 자신의 변수를 변경하더라도 프로세스 B의 변수는 변경되지 않는다. 따라서 프로세스 A 와 B가 통신을 위해서는 시그널과 같은 IPC가 필요하다.
  • 멀티 스레드는 독립적인 스택을 가지고 있으나 전역변수는 서로 공유한다. 즉, 지역변수는 독립적이지만 전역 변수는 서로 공유한다는 점을 이용하여 IPC 없이도 스레드 간의 상호 통신이 가능하다.

 

위의 확실한 한가지 차이 때문에 멀티 프로세서와 멀티스레드는 항상 비교 대상이 된다. 멀티 프로세서는 완전히 독립된 변수 환경을 가지고 있는데 비해, 멀티 스레드는 힙 영역과 전역 변수 영역을 공유한다는 점에서 큰 차이를 가지고 있다.(다만, 스택 영역은 공유하지 않으며, 지역변수는 스택에 쌓이기 때문에 상호 독립적이다.)

이러한 차이로 인해 경우에 따라 멀티프로세스보다 멀티 스레드로 프로그램을 구현하는 것이 좀 더 자연스럽고 간단한 프로그래밍이 되는 경우도 상당히 많이 있다.

 

쉘에서 실행된 각 프로그램은 새로운 프로세스로 시작된다. 예를 들어, 다음과 같은 명령 파이프라인을 실행하기 위해서 쉘은 3개의 프로세스를 만든다

 

$ ls -l | sort -k5n | less

 

본 쉘을 제외한 주요 쉘들은 모두 작업 제어라는 대화형 기능을 제공하는데, 이를 통해 사용자는 여러 명령이나 파이프라인을 동시에 실행하고 조작할 수 있다. 작업 제어 쉘에서 파이프라인 내의 모든 프로세스는 새로운 프로세스 그룹이나 작업에 속하게 된다. 프로세스 그룹 내의 각 프로세스는 동일한 프로세스 그룹 ID를 갖는데, 이 정수는 그룹 내 프로세스 중 하나의 프로세스 ID와 같다.

커널은 프로세스 그룹 내 전체 프로세스를 대상으로 시그널 전달 등 여러가지 동작을 수행할 수 있다. 작업 제어 쉘은 이 기능을 이용해서 사용자가 파이프라인 내 모든 프로세스를 중지시키거나 재개시킬 수 있게 한다.

시그널은 IPC 외에도 많은 분야에서 폭넓게 쓰이고 있다. 시그널은 '소프트웨어 인터럽트'라고도 하며, 프로세스 입장에서 시그널은 외부에서 어떤 예외상황 혹은 이벤트가 발생했음을 알려주는 트리거 역할을 한다.

시그널에는 여러 종류의 시그널이 있는데, 각각의 종류에 따라 서로 다른 이벤트나 상황을 알려준다. 각 시그널의 종류는 각기 다른 정수로 나타내는데, SIGxxxx 형태의 이름으로 정의되어 있다.

 

시그널은 커널이나 프로세스에서 다른 프로세스나 자신에게 보낸다. 예를 들어, 커널은 다름과 같은 상황에서 프로세스에게 시그널을 보낸다.

 

  • 사용자가 키보드에서 인터럽트(대표적으로 control-C)를 입력했을 때
  • 자식 프로세스 중 하나가 종료되었을 때
  • 프로세스가 설정한 타이머(알람)가 만료되었을 때
  • 프로세스가 잘못된 메모리 주소에 접근하려고 할 때

 

쉘에서 kill 명령으로 프로세스에게 시그널을 보낼 수 있다. kill() 시스템 호출도 프로그램 안에서 같은 기능을 수행한다.

 

프로세스가 시그널을 받으면, 시그널에 따라 다음 중 한 가지 동작을 수행한다.

 

  • 시그널을 무시한다.
  • 시그널을 받고 종료된다.
  • 특수 목적 시그널을 받고 재개될 때까지 중지된다.

대부분의 시그널에 대해, 기본 시그널 동작을 받아들이는 대신, 프로그램이 시그널을 무시하거나 시그널 핸들러를 설정할 수 있다. 시그널 핸들러라 함은 프로그래머가 직접 정의한 함수로, 해당 시그널이 전달되면 프로그래머가 정의한 함수가 실행되게 된다. 이 함수는 시그널이 발생한 상황에 맞춰 적절히 동작하도록 구성하여야 한다.

 

시그널이 발생한 시점에서 전달되기까지를 '시그널이 프로세스에 대해 보류 중'이라고 한다. 일반적으로 보류 중인  시그널은 수신 프로세스가 다음에 실행되면 해당 프로세스가 이미 실행 중이면 즉시 전달된다. 하지만 프로세스의 시그널 마스크에 추가함으로써 시그널을 블록 할 수 도 있다. 블록 중인 시그널이 발생되면, 나중에 블록이 해제될 때까지 시그널은 계속 보류 상태에 머물게 된다.

유닉스 I/O 모델의 특징 중 하나로 만능 I/O 개념을 들 수 있다. 디바이스를 비롯한 모든 종류의 파일에 I/O를 수행할 때 동일한 시스템 호출( open(), read(), write(), close()등)을 사용한다는 뜻이다. 따라서 이 시스템 호출을 사용하는 프로그램은 어떤 종류의 파일도 사용할 수 있다.

 

커널이 제공하는 파일은 근본적으로 한가지, 순차적인 바이트의 흐름으로, 디스크 파일, 디스크와 같이 비순차적 접근이 가능한 디바이스의 경우 lseek() 시스템 호출을 통해 임의의 위치로 접근할 수 있다.

여러 응용프로그램과 라이브러리에서 줄바꿈 문자를 텍스트의 한 줄이 끝나고 새 줄이 시작하는 것으로 해석한다. 유닉스 시스템에는 EOF (end-of-file) 문자가 없으며, 파일을 읽었을 때 데이터가 없으면 파일의 끝으로 간주한다.

 

I/O 시스템 호출은 파일 디스크립터를 통해 열려있는 파일을 참조한다. 여기서 파일 디스크립터는 보통 음수가 아닌 작은 정수이며, I/O 대상 파일의 경로명을 인자로 받는 open()을 통해 얻을 수 있으며, 한번 호출된 뒤부터는 파일이 닫힐 때까지 프로세서와 파일 간의 통로 역할을 하여 준다.

보통 프로세스는 쉘에서 실행될 때 열려있는 파일 디스크립터 3개를 물려받는다. 디스크립터 0은 표준 입력, 디스크립터 1은 표준 출력, 디스트립터 2는 표준 에러이다. 대화형 쉘이나 프로그램에서 이들 세 디스크립터는 일반적으로 터미널에 연결되어 있다. stdio 라이브러리에서 이들 디스크립터는 stdin, stdout, stderr 파일에 해당된다.

 

C 프로그래밍을 할때 가장 처음 작성하는 #include <stdio.h>가 사실은 위의 stdio 라이브러리를 사용해서 표준 출력 디스크립터, 즉 stdout을 사용해서 hello,world를 출력하기 위함이다.

리눅스 커널은 하나의 계층적 디렉터리 구조로 시스템의 모든 파일을 관리한다. 이 계층구조의 기초에는 루트 디렉터리가 있고( / ) 모든 파일과 디렉터리는 이 루트 디렉터리 하부에 위치한다.

 

파일 시스템 내 각 파일에는 파일의 종류가 표시되어 있다. 파일 종류에는 일반파일, 디바이스, 파이프, 소켓, 심볼릭 or  하드 링크, 디렉터리 등의 파일이 있다.

 

  • 디렉터리, 링크

디렉터리는 그 안에 파일 이름과 해당 파일로의 참조로 이뤄진 표를 담고 있는 특수한 파일이다. 파일 이름과 참조가 서로 연결된 것을 링크라고 하며, 파일은 여러 링크를 가질 수 있어, 같은 디렉터리나 다른 디렉터리에 여러 이름으로 존재할 수 있다.

디렉터리에는 최소한 다음의 두 가지 엔트리가 존재한다. '.'은 해당 디렉터리 자신을 가리키는 링크이고, '..'은 계층상 바로 상위 디렉터리인 부모 디렉터리를 가리키는 링크이다. 단, 루트 디렉터리( / )에서는 자신보다 상위 디렉터리가 없기 때문에 '..' 또한 자신을 가리키는 링크로 간주된다.

 

  • 심볼릭 링크

하드 링크와 마찬가지로, 심볼릭 링크도 파일의 또 다른 이름을 제공한다. 그러나 일반 링크가 디렉토리 목록 안의 파일 이름-포인터 엔트리임에 비해, 심볼릭 링크는 다른 파일 이름을 담고 있는 특별히 표시된 파일이다.(즉 심볼릭 링크는 디렉터리에 파일 이름-포인터 엔트리로 존재하고, 해당 포인터가 가리키는 파일에 다른 파일의 이름이 문자열로 들어 있다.) 심볼릭 링크가 가리키는 파일을 대상 파일이라고 하고, 흔히 심볼릭 링크가 대상 파일을 '가리킨다', '참조한다'라고 표현한다. 시스템 호출에 경로명을 적으면 대부분의 경우 커널은 자동으로 경로명 안의 각 심볼릭 링크를 역참조 한다. 즉 경로명 안의 심볼릭 링크 이름을 링크가 가리키는 파일 이름으로 교체하면서 따라간다. 이과정은 심볼릭 링크가 가리키는 대상 자체가 다른 심볼릭 링크인 경우 재귀적으로 계속 따라가게 된다.( 단 커널은 심볼릭 링크가 루프를 형성할 때는 대비해 역참조 횟수를 제한하고 있다.) 심볼릭 링크가 존재하지 않는 파일을 가리키면, 댕글링 링크라고 한다.

 

  • 파일 이름

대부분의 리눅스 파일 시스템에서 파일 이름은 최대 255자까지 가능하다. 파일 이름에는 슬래시와(/) 널 문제(\0)를 제외한 모든 문자가 포함될 수 있다. 하지만 알파벳과 숫자, '.', '_', '-'만 사용하는 것이 좋다. 이 65개의 문자 집합을 SUSv3에서는 이식성 있는 파일 이름 문자 세트라고 한다.

이식성 있는 문자 세트 외의 문자는 쉘이나 정규표현식 등에서 특별한 의미로 사용될 수 있으므로 파일 이름에는 사용하지 않는 것이 좋다. 피치 못할 사정으로 파일이름에 이러한 특수문자를 사용할 때는 해당 문자 앞에 이스케이프 문자(\)를 사용하여 이것이 단순히 문자로만 사용된다는 사실을 명시하여 주는 것이 좋다.

마찬가지로 하이픈(-)으로 시작되는 파일 이름도 피해야 한다. 그런 파일 이름은 쉘에서는 옵션으로 판단 될 수도 있기에 자칫하면 오류의 원인이 될 수 있다. 정리하자면

 

  • 파일이름은 최대 255자 까지 사용 가능하다.
  • 알파벳과 숫자, '.' , '_' , '-'만 사용한다.
  • 이외의 특수문자를 사용할 경우 특수문자 앞에 이스케이프 문자(\)를 붙여준다.
  • 파일 이름은 하이픈( - )으로 시작하지 않는다.

와 같이 볼 수 있겠다.

 

경로명은 파일 이름들이 /로 구별되어 나열된 것으로 마지막 / 이전까지를 경로명의 디렉터리 부분이라고 하고, 마지막 / 이후 부분을 베이스 부분이라고 한다. 베이스 부분은 경로명에서 존재할 수도 있지만 그렇지 않을 수도 있다.

경로명은 크게 다음의 두 가지로 나뉜다.

 

  • 절대 경로명 : /로 시작해서 루트 디렉터리를 기준으로 나타내고자 하는 파일의 위치를 표현한다. 항상 루트 디렉터리에서부터 경로를 찾아간다는 점에서 절대 경로명으로 불린다.
  • 상대 경로명 : 현재 디렉터리를 기준으로 나타내고자 하는 파일의 위치를 표현한다. 루트 디렉터리에서 경로를 찾아가지 않기 때문에 /로 시작하지 않고 ./ 혹은 ../로 시작하며 이 중./ 의 경우 생략이 가능하다.

각 프로세스에는 현재 작업 디렉터리가 있다. 이것은 프로세스의 단일 디렉토리 계층구조 내에 있는 '현 위치'이며, 해당 프로세스에서 상대 경로명을 해석할 때의 기준 디렉터리이다.

 

프로세스는 부모 프로세스로부터 현재 작업 디렉터리를 상속받는다. 로그인 쉘의 첫 작업 디렉터리는 사용자의 패스워드 엔트리에 지정된 홈 디렉토리로 설정된다. 쉘의 현재 작업 디렉토리는 cd 명령으로 바꿀 수 있다.

 

각각의 파일에는 파일의 소유자와 그룹을 정의하는 사용자 ID, 그룹 ID가 지정되어 있다. 어떠한 프로세스가 이 파일에 접근하려 할 때, 커널은 프로세스의 UID와 GID를 이 파일의 소유자인지(사용자 ID- UID가 일치하는지), 혹은 그룹의 구성원인지(그룹 ID - GID가 일치하는지), 아니면 둘 다 아니고 그 외의 프로세스인지를 확인하고 그에 따라 접근을 허용할지 여부를 결정한다. 이때, 파일에는 어디까지 파일에 대한 권한을 허용할지에 대한 3가지 접근권한 비트가 명시되어 있는데, 첫 번째는 읽기 권한, 두 번째는 쓰기 권한, 세 번째는 실행 권한에 해당한다.

 

  • 읽기 권한을 가지고 있으면 해당 파일이나 프로그램을 열람할 수 있다.
  • 쓰기 권한을 가지고 있으면 해당 파일을 갱신, 수정할 수 있다.
  • 실행 권한을 가지고 있으면 해당 스크립트나 프로그램을 실행할 수 있는 권한이다.

이 권한은 디렉터리에도 설정할 수 있는데 디렉토리는 그 성격이 조금 다르다. 읽기 권한은 디렉토리의 내용물들, 즉 파일 이름을 나열할 수 있는 권한을 말하고, 쓰기 권한은 디렉토리의 내용물을 바꿀수 있는, 디렉토리 하부에 새로운 디렉토리 혹은 파일을 추가, 제거할수 있는 권한, 실행 권한은 디렉토리 하부에 있는 파일에 접근할 수 있는 권한을 말한다.

 

즉, 특정 파일에 접근하기 위해서는 해당 파일에 대한 접근 권한도 있어야 하겠지만 파일이 해당하는 디렉토리에 대한 실행 권한 또한 가지고 있어야 한다는 것이다. 만약 둘 중 하나라도 충족하지 못한다면 파일에 접근은 커널이 차단하게 된다.

'리눅스 > 기본개념 및 용어' 카테고리의 다른 글

시그널  (0) 2019.09.23
파일 I/O 모델  (0) 2019.09.23
프로세스 간 통신과 동기화  (0) 2019.09.22
정적 라이브러리와 동적 라이브러리  (2) 2019.09.22
메모리 매핑  (0) 2019.09.22

리눅스 시스템은 수많은 프로세스로 이루어져 있고, 이 중 상당수는 상호 독립적으로 동작한다. 하지만 경우에 따라서 프로세스들은 서로 협력해서 목적을 달성하는 경우도 있는데, 이럴 때 이들은 서로 통신하고 서로의 동작을 동기화할 방법이 필요하다.

 

프로세스 간의 통신방법 중한 가지로 서로 협의가 된 디스크의 특정 파일에 정보를 쓰고, 읽는 것이다. 하지만 이러한 방법은 통신방법으로는 너무 느리고 불편하다. 직접 통신을 할 수 있는데 굳이 CPU나 메모리에 비해 한참 속도가 떨어지는 디스크(혹은 SSD와 같은 저장장치)를 사용하여 통신을 할 필요가 없는 것이다. 그래서 리눅스는 다음과 같은 방법으로 통신을 한다.

 

  • 시그널 : 이벤트가 발생했음을 알리는 통신방법
  • 파이프(pipe - | ), FIFO : 프로세스 간에 데이터를 전송해야 할 때 쓰는 통신방법
  • 소켓 통신 : 컴퓨터 안의 프로세스간에 혹은 다른 PC와 네트워크를 통해 프로세스 간에 데이터를 보낼 때 사용
  • 파일 잠금 : 말 그대로 다른 프로세스가 파일을 열람하거나 수정하지 못하도록 독점하는 행위
  • 메시지 큐 : 프로세스 간에 메시지(데이터 패킷)을 교환할 때 사용
  • 세마포어 : 프로세스간의 동작을 동기화할 때 사용
  • 공유 메모리 : 둘 이상의 프로세스가 메모리 일부를 공유할 경우 사용. 이러한 경우 공유 메모리를 사용하고 있는 프로세스들 중 어느 하나의 프로세스가 메모리 일부를 갱신하면, 다른 프로세스도 즉시 갱신된 메모리를 확인할 수 있다.

 

이러한 통신 방법을 IPC(interprocess communication) 방법이라고도 하며, 중복되는 기능들이 많은 이유는 리눅스를 포함한 많은 유닉스 시스템에는 한 가지 통신방식만 있는 것이 아니라 여러 방식으로 변화된 표준들(변종 계통이 많다)이 있고, 이들이 요구하는 표준이 각기 다르기 때문이다. 예를 들어 FIFO와 유닉스 도메인 소켓은 같은 시스템상의 독립된 프로세스 간에 데이터를 교환한다는 점에서 근본적으로 동일한 기능을 수행하지만, 이들이 어디에서 파생되어서 왔는지를 살펴보면 FIFO는 시스템 V, 소켓은 BSD에서 왔다는 점에서 둘 모두를 수용하는 과정에서 FIFO, 소켓통신 모두를 지원하고 있다고 보면 되겠다.

오프젝트 라이브러리는 컴파일된 오브젝트 코드를 담고 있는 파일로, 응용 프로그램이 호출할 수 있는 함수들이 들어 있다. 연관된 함수들을 하나의 오브젝트 라이브러리에 담으면 프로그램 생성과 유지보수가 편리해진다. 현대 유닉스 시스템은 정적 라이브러리와 동적 라이브러리라는 두 가지 오브젝트 라이브러리를 제공한다.

 

 

  • 정적 라이브러리

초기 유닉스 세스템에서는 정적 라이브러리밖에 존재하지 않았다. 정적 라이브러리는 쉽게 말하면, 컴파일된 오브젝트 모듈의 체계적인 묶음이다. 정적 라이브러리의 함수를 쓰기 위해서는, 프로그램을 빌드하는 과정 중 링크 과정에서 해당 정적 라이브러리를 명시함으로써 해당 라이브러리에 포함되어 있는 여러 함수를 프로그램 내 코드에 필요한 정의를 복사해서 넣는다. 이러한 프로그램을 정적으로 링크된 프로그램이라고 한다.

정적 라이브러리를 사용할 경우 해당 라이브러리를 사용하는 여러 프로그램이 중복되는 코드가 여기저기 산재해 있어 커널에 설치되는 프로그램의 수가 많아질 수록, 혹은 여러 프로그램을 동시에 멀티 태스킹을 해야 할 경우, 각각의 프로그램이 필요 이상으로 덩치가 커지는 문제가 있다. 다시 한번 정리해서 말하자면, 정적으로 링크된 각 프로그램들은 라이브러리에서 추출된 오브젝트 모듈들의 복사본을 각각 포함하고 있기 때문에, 오브젝트 코드가 중복되어 디스크 공간이 낭비되고, 같은 라이브러리 함수를 쓰는 프로그램들이 동시에 실행될 때 또한 메모리가 낭비된다는 단점이 있다.

또한 정적 라이브러리는 라이브러리 함수를 수정해야 하는 상황이 발생하였을 경우, 정적 라이브러리만 수정하는 것으로 끝나는 것이 아니라 이미 이러한 라이브러리를 사용한 모든 프로그램들을 찾아내서 새로이 링크 작업을 거쳐 실행 파일을 생성하여야 한다는 치명적인 단점이 있으나, 한번 실행파일을 생성하면, 정적 라이브러리가 추후 제거되더라도 이미 생성된 실행파일을 실행하는 데는 아무런 문제가 없다는 장점으로 인해 많은 단점에도 불구하고 아직까지 사용되고 있다.

 

  • 동적 라이브러리

동적 라이브러리는 다른 말로 공유 라이브러리라고도 불리우며 정적 라이브러리의 문제를 해결하기 위해 만들어졌다.

프로그램이 동적 라이브러리와 링크되면, 라이브러리 오브젝트 모듈을 실행 파일로 복사하는 대신, 링커는 실행 실행 파일이 실행 시에 해당 공유 라이브러리가 필요하다는 정보만 기록하여 둔다. 이후 실행 파일이 실제로 실행되어 메모리에 올라가면, 동적 라이브러리에 속한 함수가 필요할 때 라이브러리 정보를 참조하여 실제 동적 라이브러리를 참조하여 필요한 정보를 메모리에 올리게 된다. 이후 다른 프로그램이 실행되며 같은 라이브러리 함수를 참조하고자 하면, 이미 메모리에 존재하는 동적 라이브러리 정보를 참조하여 바로 링크 작업을 하게 된다. 이러한 특성 때문에 공유 라이브러리라고도 불리고 있다. 또한 동적 라이브러리는 수정이 필요할 경우 라이브러리만을 수정하여도 기존의 프로그램들이 수정된 라이브러리를 사용한다는 점에서 유지보수가 매우 쉬운 편에 속한다. 하지만, 그럼에도 불구하고, 수정이 잘못되거나, 삭제될 경우 이 라이브러리를 사용하는 프로그램 또한 오작동, 혹은 실행이 불가능해진다는 단점이 있어서 무조건 정적 라이브러리보다 좋은 것은 아니다.

 

 

커널은 위 두 라이브러리의 장단점을 각 라이브러리 성격에 대비하여, 적절한 방식을 사용하여 라이브러리를 구성하고 있다.

리눅스에서는 mmap() 호출을 통해 프로세스의 가상 주소 공간에 새로운 메모리 매핑을 만들 수 있다.

이러한 매핑에는 두 가지 종류가 있다.

 

  • 파일 매핑 : 파일의 일부를 프로세스의 가상 메모리로 매핑한다. 매핑되면, 해당 메모리 영역을 통해 파일의 내용에 접근할 수 있다. 매핑된 페이지는 파일로부터 필요에 따라 자동으로 로드된다.
  • 익명 매핑 : 파일에 매핑되지 않고, 매핑된 페이지들이 0으로 초기화된다.

 

프로세스에 매핑된 메모리는 다른 프로세스와 공유가 가능하다. 메모리 공유는 여러 프로세스가 같은 파일 영역을 매핑하거나, fork()를 통해 새로운 프로세스가 생성될 때 자식 프로세스가 매핑을 그대로 물려받는 경우 발생할 수 있다.

 

둘 이상의 프로세스가 같은 페이지를 공유하면, 매핑이 공유인지, 비공개인지에 따라 공유된 페이지에서 다른 프로세스가 수정, 변경한 내용을 볼 수도 있다.

 

  • 공유 매핑의 경우, 다른 프로세스에서 변경된 사항이 현재 프로세스에서도 보이고, 실시간 반영된다.
  • 비공개 매핑의 경우, 다른 프로세스에서 변경된 사항이 보이지 않고, 실시간 반영또한 되지 않는다.

 

메모리 매핑은 실행 파일의 해당 세그먼트를 이용한 프로세스의 텍스트 세그먼트 초기화, 새로운 메모리 할당, 프로세스 간 통신, 파일 I/O 등 여러 용도로 사용될 수 있다.

프로세스는 쉽게 말하면 실행 중인 모든 각각의 프로그램을 말한다.

즉, 지금 이 페이지를 보고 있는 웹 브라우저 또한 커널 입장에서 보면 프로세스라고 볼 수 있다.

이러한 프로그램이 실행될 때는 커널이 프로그램의 정보를 정의한 바이너리 코드를 메모리에 올리고, 프로그램을 위한 변수 공간을 할당하고, 프로세스에 대한 정보를 담을 별도의 데이터 구조를 준비하게 된다.

 

커널이 보기에 이 프로세스들은 전부 자신이 관리하고 있는 컴퓨터 제원을 나눠서 할당하고, 관리해야 할 대상이 된다.

여기서 메모리나 CPU와 같이 무한정 제공되지 않는 한정된 자원의 경우, 커널은 처음에는 최소한의 자원만을 할당하고, 프로세스가 존재하는 동안 각각의 프로세스들이 요구하는 자원의 정도를 확인하고 적절하게 추가 배분하는 등 프로세스의 전체적인 조율을 하는 역할을 하게 된다. 이후 종료되는 프로세스가 있다면 할당하였던 자원을 반납받고, 필요로 하는 다른 프로세스에게 자원을 할당하거나 여유자원으로 가지고 있게 된다.

 

프로세스는 다음의 세그먼트로 구분할 수 있다.

 

  • 텍스트 : 프로세스의 정의,명령
  • 데이터 : 프로그램이 사용하는 정적 변수
  • 힙 : 프로그램이 사용하는 동적 변수(프로그램이 필요에 따라 추가적으로 변수 공간 할당을 요청하면 이곳에 할당)
  • 스택 : 함수의 호출, 반환에 따라 쌓이고, 줄어드는 메모리 영역

 

프로세스는 fork() 호출을 통해 새로운 프로세스를 만들 수 있다. 이때, 호출을 하는 프로세스를 부모 프로세스, 호출에 따라 새로이 생성되는 프로세스를 자식 프로세스라고 한다. 프로세스가 fork()를 호출하면 커널은 이 요청을 받아들여 부모 프로세스의 모든 것(변수, 현재 상태 등 모든 정보)들을 복사해서 새로운 프로세스를 만들어 내게 된다. 이렇게 새로 생성된 자식 프로세스는 생성이 끝난 후부터는 독립된 프로세스로서 부모 프로세스와는 다른 변수를 생성할 수도, 삭제할 수도, 별도의 독립된 함수를 실행할 수도 있게 된다. 혹은, execve() 호출을 통해 기존의 부모 프로세스로부터 물려받은 모든 세그먼트(텍스트, 데이터, 힙, 스택)들을 제거하고 완전히 새로운 코드에 따라 세그먼트를 구성할 수도 있다.

 

각각의 프로세스에는 고유한 ID(고유한 정수 - 다른 프로세스와 구분하기 위한 유일한 숫자)를 가지고 있다. 이를 PID라고 부르며, 마찬가지로 각각의 프로세스에는 자신을 생성한 부모 프로세스 또한 가지고 있다. 이것을 PPID라고 부른다.

 

프로세스는 보통 두 가지 방식으로 종료될 수 있다. _exit() 시스템 호출(or exit() 라이브러리 함수)을 통해 스스로 종료하기를 커널에게 요청하거나, 외부로부터 종료 시그널을 받아 종료되는 경우가 있다.

이때 두 가지 경우 모두 왜 종료하였는 지, 어떤 방식으로 종료되었는지를 알리는 종료 상태를 생성되는데, 프로세스가 스스로 종료하는 경우 종료 상태를 스스로 생성해서 반환하고, 외부에서 종료시키는 경우 외부에서 온 시그널이 어떤 종류의 시그널이냐에 따라 종료 상태가 결정된다.

이러한 종료 상태는 보통 0은 정상종료를 의미하고, 0이 아닌 모든 수를 비정상 종료, 즉 에러가 발생함에 따라 종료된 것을 뜻한다. 대부분의 쉘에서 마지막으로 실행된 프로그램의 종료 상태를 $?라는 쉘 변수를 통해서 얻을 수 있다.

 

각 프로세스는 다음과 같은 여러 사용자 ID(UID)와 그룹 ID(GID)가 관련되어 있다.

 

  • 실제 사용자 ID 와 실제 그룹 ID : 프로세스가 속한 사용자, 그룹. 새 프로세스는 이 정보를 부모로부터 물려받는다.
  • 유효 사용자 ID 와 유효 그룹 ID : 프로세스 간의 통신 객체와 같은 보호된 자원에 대한 접근권한을 결정하기 위해 쓰인다. 보통은 유효 ID와 실제 ID가 같지만, 특별한 경우 유효 ID를 바꾸게 되면, 프로세스가 다른 사용자나 그룹의 권한을 가질 수 있게 된다. 이러한 UID와 GID는 부모로부터 물려받게 된다.
  • 보조 그룹 ID : 프로세스가 속한 추가적인 그룹. 마찬가지로 부모로부터 물려받게 된다.

 

유닉스에서 특권 프로세스는 유효 사용자 ID가 0인(슈퍼유저)인 프로세스를 말한다. 이런 프로세스는 관리자 권한을 가지고, 커널이 적용하고 제한하는 대부분의 권한 제한에서 자유롭다. 이외의 프로세스는 비특권 프로세스라고 불리며, 커널이 강제하는 권한 제한을 적용받아, 해당 권한을 얻기 위해선 커널의 승인을 받아야 한다.

 

위의 내용들을 토대로 생각할 수 있지만 언급하자면, 특권 프로세스가 생성한(커널에게 생성을 요청한) 프로세스는 마찬가지로 특권 프로세스가 된다. 왜냐하면, 자식 프로세스는 부모로부터 유효 UID와 GID를 물려받기 때문이다.

비특권 프로세스가 특권 프로세스가 되는 방법은 set-user-ID 방식으로, 프로세스를 실행한 프로그램 파일의 사용자 ID가 그 프로세스의 유효 사용자 ID가 된다.

 

시스템을 부팅하면 커널은 /sbin/init 프로그램 파일을 실행하여 모든 프로세스의 부모 프로세스인 init 프로세스를 만든다. init 프로세스의 ID는 1이고, 언제나 슈퍼유저의 특권을 가지고 있다. init 프로세스는 어떠한 경우에도 종료시킬 수 없으며(그럴 일은 없겠지만 init프로세스가 종료되면 커널은 그 순간 동작을 멈추게 된다. 그렇기에 슈퍼유저 권한을 가지고 있더라도 강제로 종료시킬 수가 없다.) 커널이 종료되는 순간에만 같이 종료된다. init 프로세스는 주로 여러 프로세스를 생성하고, 정상 동작하는지에 대한 감시를 하게 된다.

 

데몬 프로세스는 많이들 들어보았을 것이다. 이 프로세스는 다른 프로세스와는 구별되는 몇 가지 특징이 있다.

 

  • 누군가가 강제로 종료시키지 않는 이상 커널이 부팅될 때부터 커널이 종료될 때까지 계속 존재하고 있다
  • 보통 백그라운드에서 실행되고 제어받는 대상이 아니다.
  • 다른 프로세스를 보조하거나, 다른 프로세스의 행위를 기록하는 일을 한다.(게임으로 치면 액티브 스킬이 아니라 패시브 스킬이라고 보면 된다.)

 

이러한 데몬 프로세스로 시스템 로그 메시지를 기록하는 syslogd, 우분투 리눅스에서 영어가 아닌 한글을 입력하기 위해 존재하는 iBus-hangul, 데이터베이스 서버 등이 있다.

 

각 프로세스는 환경 목록이라는 환경변수의 집합을 가지고 있다. 이 목록의 각 요소는 이름과 값의 쌍으로 이루어져 있고, fork()를 통해 새로운 프로세스가 만들어지면, 이 환경 목록을 그대로 복사해서 물려주게 된다.(사실은 이러한 과정이 있기 때문에 자식 프로세스가 부모프로세스로 부터 각종 정보를 물려받을 수 있는 것이다.) 이 환경목록 또한 execve() 함수를 통해 새로이 지정된 환경으로 대치하여 물려받을 수도 있다.

 

각 프로세스는 열람 중인 파일, 메모리, CPU 시간 등의 자원을 항상 소비한다. setrlimit() 호출을 하면 이러한 프로세스의 자원 사용에 상한선을 지정할 수 있다. 이러한 자원 한도는 연성한도 와 경성 한도의 쌍으로 이루어지는데, 연성 한도는 프로세스가 소비할 수 있는 자원의 한도, 경성 한도는 연성 한도를 조절할 수 있는 상한선을 말한다.

비특권 프로세스는 특정 자원의 연성 한도를 0부터 경성 한도까지의 범위에서 자유롭게 변경해서 사용할 수는 있으나, 경성 한도는 오직 낮출 수만 있고 올려 잡을 수는 없다. 즉, 경성 한도는 무조건 지켜져야 하는 외부에서 지정한 최대한도, 연성 한도는 자체적으로 조율이 가능한 내부 한도라고 볼 수 있다.

 

fork()를 통해 새로운 프로세스가 생성되면, 부모의 자원 한도 설정 또한 물려받게 된다.

 

+ Recent posts