CS/운영체제

운영체제 - 스레드 안전, IPC, 좀비 프로세스

개발 일기92 2024. 6. 12. 10:40

스레드 안전?

멀티 스레드 환경에서 하나의 변수, 함수, 객체에 스레드 여러개가 동시에 접근해도 문제가 없음을 의미.

 

안전하지 않은 경우?

ex) value++;

위 코드는 value 변수의 값을 메모리에서 CPU 레지스터로 로드 -> 연산 처리 -> 연산 결과를 메모리에 작성하는 과정을 거치게 되는데 만약 2개이상의 스레드가 해당 코드에 접근하면 잘못된 결과를 초래할 수 있다.

 

스레드 2개가 각각 value값이 0일때 접근하면 1을 메모리에 저장하며 종료되겠지만,

스레드 1이 먼저 value값에 1을 저장한 후 스레드 2가 접근할 시 value값은 2가된다.

 

이전 글에 포스트한 우유문제와 비슷한 현상이다.

 

스레드 안전을 위한 조건

  • 상호배제(mutual exclusive) : 공유 자원에 접근해야 할 때 뮤텍스나 세마포어와 같은 기법을 통해 접근을 통제한다.
  • 원자 연산(atomic operation) : 공유 자원에 접근 할때 원자 연산을 이용하거나 원자적으로 정의된 연산을 이용해 연산 도중에 다른 스레드가 접근할 수 없게 한다. ( 원자연산? : 연산했다 or 연산 안 했다 두가지만 존재하는 연산)
  • 재진입성(reentrancy) : 특정 함수를 하나의 스레드에서 실행 중일 때 다른 스레드가 해당 함수를 실행하더라도 각 스레드에 올바른 결과가 나올 수 있게 해야한다.
  • 스레드 지역 저장소(tread local storage) : 각 스레드에서만 접근할 수 있는 저장소를 사용해서 공유되는 자원을 줄여야 한다.

TLS? (tread local storage)

  • 쓰레드가 각각 가지고 있는 저장소이다.
  • 전역변수 이지만, 한 쓰레드 내에서만 유효한 전역 변수이다. (Stack과는 다름)
  • 이 TLS을 이용해서 쓰레드가 집중된 task를 빠르게 처리하는가? 하면, 공유하는 영역인 Heap, 데이터 영역의 값을 사용 할 만큼 큼직하게 잘라서 TLS로 가져온다. 이 과정만 하기 때문에 부하가 적다. 이렇게 공유영역에서 가져온 값을 TLS로 옮기면 그 이후에 mutex를 차지하기 위한 경합이 일어나지 않는다.
thread_local int32 LThreadId = 0 ; // thread_local을 작성 하는 것으로 TLS임을 알려준다.

void ThreadMain(int32 threadId)
{
	LThreadId = threadId;
    
    while ( true)
    {
    	cout << "Hi I am Thread" << LThreadId << endl;
        this_thread::sleep_for(1s);
    }
}


int main(){
	vector<thread> threads;
    
    for( int32 i = 0 ; i<10 ; i++)
    {
    	int32 threadId = i +1;
        threads.push_back(thread(ThreadMain,threadId));
    }
    
    for(thread& t : threads)
    	t.join();
}

 

※TLS 정보글 출처 : https://velog.io/@onenewarm/Thread-Local-StorageTLS

 


IPC?(Inter Process Communication)

프로세스는 고유한 메모리 영역을 갖기 때문에 프로세스 간 자원을 공유해야 할 때 IPC를 해야한다.

 

IPC의 대표적인 종류

1. 공유 메모리 : 프로세스 간에 공유 가능한 메모리를 구성해 자원을 공유하는 방식.

여러 프로세스에서 접근 할 수 있으므로 동기화 문제가 발생할 수 있다.

 

2. 소켓 : 네트워크 소켓을 이용하는 프로세스 간 통신으로, 외부 시스템과도 이용할 수 있다. 

클라이언트 <-> 서버 구조로 자원을 주고 받는다.

 

3. 세마포어 : 접근하는 프로세스를 제어해 공유 자원을 관리한다.

 

4. 파이프 : FIFO 형태의 파이프를 이용해 프로세스간 자원을 공유. 

파이프는 단방향 통신만 지원하므로 양방향 통신을 하려면 읽기, 쓰기 파이프를 각각 생성해야한다.

 

5. 메세지 큐 : FIFO 형태의 큐 자료구조를 사용해 프로세스간 메세지를 주고 받는 형식이다.

 


좀비 프로세스?

자식 프로세스가 종료되었지만 부모 프로세스가 자식 프로세스의 종료 상태를 회수하지 않았을 경우 남겨진 자식 프로세스를 좀비 프로세스라고 한다.

자식 프로세스가 종료될 때 부모 프로세스에 SIGHLD라는 시그널을 보내면 부모 프로세스에서 wait() 시스템 콜 함수를 호출하여 자식의 상태 정보를 받고 자원을 회수한다. 이때 자원 회수에 실패하면 좀비 프로세스가 생긴다. ( 많이 쌓이면 자원 낭비 ↑)

 

고아 프로세스?(orphan process)

부모 프로세스가 자식보다 먼저 종료되는 경우.

이런 상황에서는 부모PID를 init프로세스의 PID인 1로 바꿔준다. (모든 프로세스의 조상에게 입양한다고 이해!)

위와 같이 했을 경우 고아 프로세스의 부모 프로세스는 init프로세스가 된다. 

이후 고아 프로세스가 작업을 종료하면 init프로세스가 고아 프로세스의 자원을 회수하여 좀비 프로세스가 되는 것을 방지 할 수 있다. (자원 반환하지 못한 고아 프로세스를 위해 wait() 시스템콜을 주기적으로 호출하기 때문)