YJcode :: YJcode

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

 

  • 멀티 프로세스는 각각의 독립적인 변수 환경을 가지고 있다. 따라서 프로세스 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를 출력하기 위함이다.

+ Recent posts