728x90

프로세스와 스레드

  • 멀티태스킹과 멀티프로세싱
  • 프로세스와 스레드
  • 스레드와 스케줄링
  • 컨텍스트 스위칭

 

멀티태스킹과 멀티프로세싱

구분 관점 설명
멀티태스킹 소프트웨어
(운영체제)
하나의 컴퓨터 시스템이 동시에 여러 작업을 수행
(CPU 사용 시간을 분할하여 동시에 작업이 수행하는 것처럼 보임)
멀티프로세싱 하드웨어 둘 이상의 프로세서(여러 개의 CPU 코어)를 사용하여 여러 작업을 동시에 처리

 

프로세스와 스레드

구분 설명
프로세스 실행중인 프로그램을 프로세스라고 함
(자바로 비유하면 클래스는 프로그램, 인스턴스는 프로세스)
스레드 프로세스 내에서 실행되는 작업의 단위

 

 

프로세스의 메모리 구성

  • 코드 섹션: 실행할 프로그램의 코드가 저장되는 부분
  • 데이터 섹션: 전역 변수 및 정적 변수가 저장되는 부분(그림에서 기타에 포함)
  • 힙 (Heap): 동적으로 할당되는 메모리 영역
  • 스택 (Stack): 메서드(함수) 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역(스레드에 포함)

그렇다면 JVM은 프로세스 구성 요소 중 어디로 올라가는가? → Heap 영역

 

 

스레드와 스케줄링

작업(스레드)이 실행되기 위해서는 CPU를 할당받아서 연산을 실행해야 함

  • 실제로 CPU에 의해 실행되는 단위는 스레드)
  • 프로세스는 스레드들의 컨테이너 역할을 수행 (스레드들의 실행 환경을 제공)

이를 위해 OS는 스케줄링을 통해 스레드를 어떻게, 얼마나 오랫동안 CPU를 할당할지 결정 (CPU 시간을 여러 작업에 나누어 배분)

 

 

컨텍스트 스위칭

멀티태스킹을 위해 CPU가 여러 작업(스레드)을 번갈아가며 수행

스레드를 번갈아가며 호출하기 위해 CPU는 스레드의 상태를 저장하고 복원하는 작업을 수행

이러한 과정을 컨텍스트 스위칭이라고 함

  • 멀티스레드는 대부분 효율적이지만, 컨텍스트 스위칭 과정이 필요하므로 항상 효율적인 것은 아님

 

 

최적의 CPU 수, 최적의 스레드 수

일반적으로 스레드가 하는 작업은 CPU 바운드 작업, I/O 바운드 작업으로 구분

  • CPU 바운드 작업: CPU를 많이 사용하는 작업
    • 복잡한 수학 연산, 데이터 분석, 비디오 인코딩
  • I/O 바운드 작업: I/O 작업(파일 읽기/쓰기, 네트워크 통신 등)이 많은 작업
    • 데이터베이스 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리

일반적으로 웹 애플리케이션 서버는 CPU 바운드 작업 보다는 I/O 바운드 작업이 많음

I/O 바운드 작업이 많다면 CPU를 거의 사용하지 않고 대기

  • 따라서 CPU 코어 수에 맞게 스레드를 생성하면 CPU를 효율적으로 사용할 수 없음
  • 이 때는 스레드 수를 증가시켜 CPU를 최대한 활용할 수 있도록 함

 

CPU-바운드 작업: CPU 코어 수 + 1개

  • CPU를 거의 100% 사용하는 작업이므로 스레드를 CPU 숫자에 최적화

I/O-바운드 작업: CPU 코어 수 보다 많은 스레드를 생성

  • CPU를 최대한 사용할 수 있는 숫자까지 스레드 생성 CPU를 많이 사용하지 않으므로 성능 테스트를 통해 CPU를 최대한 활용하는 숫자까지 스레드 생성
  • 단 너무 많은 스레드를 생성하면 컨텍스트 스위칭 비용도 함께 증가 - 적절한 성능 테스트 필요
728x90
728x90

예제 코드 설명

  • 하나의 스레드는 전역 변수 target을 계속 더해주고(target++), 다른 하나의 스레드는 target을 계속 빼주는(target--) 멀티 스레드 예제 코드입니다.
  • 스레드 동기화(Thread Synchronization)없이 두 개의 스레드가 경쟁하는 구조입니다.

Windows

  • OS : Windows 10
#include <windows.h> // WinAPI 사용 
#include <stdio.h>

int target = 0;

int WINAPI addTarget(LPVOID param) { // 파라미터로 원래 LPVOID 타입을 사용하지만 void * 를 사용해도 같음 
	int limit = *(int *) param;
	int i=0;
	for (i=1; i<=limit; i++) {
		printf("[ADD] target : %d\n", ++target);
	}
	return 0;
}

int WINAPI subtractTarget(LPVOID param) {
	int limit = *(int *) param;
	int i=0;
	for (i=1; i<=limit; i++) {
		printf("[SUBTRACT] target : %d\n", --target);
	}
	return 0;
}

int main() {
	int threadId1;
	int threadId2;
	HANDLE threadHandle1;
	HANDLE threadHandle2;
	int param=100;
	
	// 스레드 생성
	threadHandle1 = CreateThread(NULL, 0, addTarget, &param, 0, &threadId1);
	threadHandle2 = CreateThread(NULL, 0, subtractTarget, &param, 0, &threadId2);
	
	Sleep(3000);
	printf("target : %d\n", target);
    return 0;
}


동기화를 안 했기 때문에 두 개의 스레드가 각자 돌아가면서 값을 출력한다.

Linux

  • OS : Ubuntu 16.04 LTS
  • GCC : 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int target = 0;

void * addTarget(void * param)
{
  int limit = *(int *) param;
  int i=0;
  for (i=1; i<=limit; i++) {
    printf("[ADD] target : %d\n", ++target);
  }
}

void * subtractTarget(void * param)
{
  int limit = *(int *) param;
  int i=0;
  for (i=1; i<=limit; i++) {
    printf("[SUBTRACT] target : %d\n", --target);
  }
}
 
int main()
{
  pthread_t add, sub;
  int param = 100;
  
  int add_id = pthread_create(&add, NULL, addTarget, &param);
  // error handling, 정상적으로 생성되면 0 반환
  if (add_id < 0)
  {
      perror("thread create error : ");
      exit(0);
  }
  
  int sub_id = pthread_create(&sub, NULL, subtractTarget, &param);
  if (sub_id < 0)
  {
      perror("thread create error : ");
      exit(0);
  }
 
  sleep(3000);
  printf("target : %d\n", target);
 
  return 0;
}

  • gcc 컴파일 시 -lpthread 옵션 추가
728x90

'운영체제' 카테고리의 다른 글

[운영체제] 프로세스와 스레드  (1) 2024.09.18
728x90

Signaling 예제 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

// 핸들러 함수 정의
void handlerFunc(int sig) {
  printf("handlerFunc() 호출됨\n");
  // SIGINT : 터미널 인터럽트
  // SIG_DFL : 기본 행동 수행, 메인 함수가 하던 일을 계속 수행하고 한 번 더 SIGINT가 오는 경우 프로세스 종료
  signal(SIGINT, SIG_DFL);
}

int main() {
  // SIGINT(키보드 인터럽트) 신호를 받으면 handlerFunc 수행
  signal(SIGINT, handlerFunc);

  int count = 0;
  while(1) {
    printf("count : %d\n", count++);
    sleep(1);
  }
  exit(0);
}

실행 결과

시그널 타입

  • SIGINT : 키보드 인터럽트
  • SIGFPE : 부동 소수점 예외
  • SIGKILL : 프로세스 종료
  • SIGCHLD : 자식 프로세스가 정지 또는 종료
  • SIGSEGV : 세그먼트 오류(비정상 종료, 메모리 엑세스 오류)
728x90

'운영체제 > IPC' 카테고리의 다른 글

[OS] IPC - Message Passing  (0) 2022.12.26
[OS] IPC - Shared Memory  (0) 2022.12.26
728x90

프로세스, 스레드, IPC에 관한 내용은 이 글을 참고하자

Message Passing 예제

sender.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define BUFFER_SIZE 1024

// 메세지 형태 정의
typedef struct {
  long msgtype;
  int value;
  char buf[BUFFER_SIZE];
} msgbuf;

int main() {
  int key_id;
  msgbuf mybuf;
  int count = 0;
 
  // 메세지 생성
  key_id = msgget((key_t) 1234, IPC_CREAT|0666);
  if (key_id == -1) {
    perror("msgget() error");
    exit(0);
  }
  
  // 메세지 타입 설정
  mybuf.msgtype = 1;
  while (1) {
    // value 값 1 증가시켜서 메세지 전송
    mybuf.value = count++;
    printf("value : %d\n", mybuf.value);
    
    if (msgsnd(key_id, &mybuf, sizeof(msgbuf), IPC_NOWAIT) == -1) {
      perror("msgsnd() error");
      exit(0);
    }
    
    sleep(10);
  }
}

receiver.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define BUFFER_SIZE 1024

// 메세지 형태 정의
typedef struct {
  long msgtype;
  int value;
  char buf[BUFFER_SIZE];
} msgbuf;

int main() {
  int key_id;
  msgbuf mybuf;
  long msgtype = 1; // 수신받을 메세지 타입 미리 설정
 
  // 메세지 생성
  key_id = msgget((key_t) 1234, IPC_CREAT|0666);
  if (key_id == -1) {
    perror("msgget() error");
    exit(0);
  }
  
  while (1) {
    // 메세지 타입이 1 일 때 수신
    if (msgrcv(key_id, &mybuf, sizeof(msgbuf), 1, 0) == -1) {
      perror("msgrcv() error");
      exit(0);
    }
    
    // 수신 받은 메세지에서 value 출력
    printf("value : %d\n", mybuf.value);
  }
}

실행화면

헤더 파일 및 주요 함수

헤더 파일

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg)

  • 메세지를 생성

int msgsnd(int msqid, struct msgbuf * msgp, size_t msgsize, int msgflg)

  • 메세지 전송

ssize_t msgrcv(int msgid, struct msgbuf * msgp, size_t msgsize, long msgtype, int msgflg)

  • 메세지 수신
728x90

'운영체제 > IPC' 카테고리의 다른 글

[OS] Signaling  (0) 2022.12.26
[OS] IPC - Shared Memory  (0) 2022.12.26
728x90

Process

  • 컴퓨터에서 실행되고 있는 프로그램
  • 실행되고 있는 프로그램의 인스턴스
  • 프로세스간 독립적
  • CPU로부터 메모리 할당

Thread

  • 프로세스 내에서 실행되는 흐름의 단위
  • 프로세스가 할당받은 자원을 이용
  • 같은 메모리 공간 공유

IPC

  • 프로세스는 독립적으로 실행되기 때문에 메모리를 공유할 수 없다.
  • 따라서 프로세스간 통신을 통해 서로 데이터를 주고 받을 수 있게 해야한다.
  • 방법은 다음과 같다.

1) Message Passing

  • 커널을 통해 메시지를 전달
  • 안전하지만 성능이 떨어짐 (Overhead가 큼)
  • 예제

2) Shared Memory

  • 프로세스간 공유된 메모리를 생성 후 이용
  • 성능이 좋지만 동기화 문제 발생 가능
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
  int shmid;
  int pid;
  
  int * cal_num;
  void * shared_memory = (void *) 0;
  
  // 공유 메모리 공간 생성
  shmid = shmget((key_t) 1234, sizeof(int), 0666|IPC_CREAT);
  
  // shmget() 실패 시 -1 return
  if (shmid == -1) {
    perror("shmget failed : "); // 오류 메시지 출력
    exit(0);
  }
  
  printf("shmid return value : %d\n", shmid);
  
  // 공유 메모리를 사용하기 위해 프로세스 메모리 부착
  shared_memory = shmat(shmid, (void *)0, 0);
  if (shared_memory == (void *) -1) {
    perror("shmat failed : ");
    exit(0);
  }
  
  cal_num = (int *) shared_memory;
  // 자식 프로셋스 생성
  pid = fork();
  
  if (pid == 0) {
    // 자식 프로세스
    shmid = shmget((key_t) 1234, sizeof(int), 0);
    
    if (shmid == -1) {
      perror("shmget failed : "); // 오류 메시지 출력
      exit(0);
    }
    shared_memory = shmat(shmid, (void *) 0,  0666|IPC_CREAT);
    if (shared_memory == (void *)-1) {
      perror("shmat failed : ");
      exit(0);
    }
    cal_num = (int *) shared_memory;
    *cal_num = 1;
    
    while(1) {
      *cal_num = *cal_num + 1;
      printf("child %d\n", *cal_num);
      sleep(1);
    }
  } else if (pid>0) {
    // 부모 프로세스
    while(1) {
      sleep(1);
      printf("*cal_num : %d\n", *cal_num);
    }
  }
}

int shmget(key_t , key, size_t size, int flags)

- #include  에 포함
- 새 공유 메모리 세그먼트를 생성하거나 키를 찾는데 사용
- 0666 | IPC_CREAT
0666 : Linux에서 일반적인 엑세스 권한, 메모리 세그먼트 권한 지시
IPC_CREAT : 공유 메모리에 대한 새 메모리 세그먼트를 만들도록 지시
- 참고 : [What is the use of IPC_CREAT | 0666 Flag in shmget()?](https://stackoverflow.com/questions/40380327/what-is-the-use-of-ipc-creat-0666-flag-in-shmget-function-in-c) - Stackoverflow

void shmat(int id, const void addr, int flags)

- #include <sys/shm.h> 에 포함
- 공유 메모리 세그먼트를 프로세스에 연결하는데 사용 ⇒ 메모리 내용에 엑세스 가능

fork()

- 자식 프로세스 생성

 

 
728x90

'운영체제 > IPC' 카테고리의 다른 글

[OS] Signaling  (0) 2022.12.26
[OS] IPC - Message Passing  (0) 2022.12.26

+ Recent posts