IT_Study/Operating System

Operating System (12) : System Architecture, Memory, Void pointer, Thread / Process Synchronization

__Vivacé__ 2023. 4. 4. 09:25

System Architecture

소프트웨어 및 하드웨어 구성 요소를 조합하여 시스템을 설계하는 방법론

 

 

폰 노이만 아키텍처 (Pon von Neumann architecture)

디지털 컴퓨터의 구조 중 하나로, 중앙 처리 장치, 메모리, 입력 및 출력 장치로 구성

1. CPU (CPU를 만드는 회사는 왜 대표적으로 Intel / AMD)
    - 0과 1로 구성된 명령어를 하나씩 수행하는 장치
    - 명령어를 저장할 공간이 부족하므로, Disk에 저장해둔다
        --> disk의 병목으로 메모리 등장 (폰 노이만의 아이디어) : CPU는 매우 빠른데, Disk 느리다.

2. Memory
    - 빠른 성능, 저장장치처럼 저장하는 기능을 가짐, 전원을 끄면 모두 소멸
    - 명령어들을 대신 전달해주는 역할
        - Loading : 저장장치에 저장된 0과 1이 메모리에 한꺼번에 복사 (시간이 걸리는 과정)
        - 이후 메모리가 CPU에게 명령 전달 (Disk보다는 빠르지만, CPU보다는 느림)

ex) Disk 속도 : 걷기 속도  |  Memory 속도 : 람보르기니 (300km/h)  |  CPU : 전투기 (2000km/h)

 

 

CPU의 대기 시간이 생기므로, 더 빠른 메모리를 내장 : SRAM

DRAM에서 SRAM으로 여러 줄의 명령어를 한꺼번에 전달

즉, Disk --> DRAM --> SRAM --> CPU 로 명령 전달

 

이 SRAM을 Cache 메모리, DRAM을 Main 메모리라고도 불린다.

 

DRAM
(Dynamic Random Access Memory)

SRAM
(Static Random Access Memory)

접근 속도 느림 빠름
사용 용도 주 기억 장치 캐시 메모리
집적도  높음 낮음
소비전력 적다 많다

프로그램 (Program) vs 프로세스 (Process) vs 프로세서 (Processor>

1. 프로그램 : CPU의 2진수 명령어들의 집합

2. 프로세스 : 프로그램을 실행(메모리에 로드)한 결과

3. 프로세서 : 프로그램의 명령을 실행하는 컴퓨터의 하드웨어


메모리의 역할

 1. 0과 1로 된 CPU 명령어를 저장하는 역할

 2. 변수가 만들어지는 공간

메모리는 공간을 나누어서 활용한다 / 그러나 연속적인 물리적 Address를 갖진 않음

 

.text : 코드 영역
.data : 초기화된 전역변수 (하드코딩 데이터)
.bss : 초기화 안 된 전역변수 (초기값 없음)

.heap : malloc으로 만든 변수들
.stack : 지역변수

*전역변수를 두 부분으로 나누는 이유 : 메모리 사용 효율과 프로그램 실행 파일 크기를 최적화하기 위해서

 

메모리 주소 확인 예제

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4
  5 int t;
  6 int g = 32;
  7
  8 int main(){
  9     int q = 31;
 10     int *p = (int*) malloc(sizeof(4));  // 좌측은 .stack, 우측은 .heap에 저장
 11
 12     while (1){
 13         printf("DATA(%d) = 0x%X\n", g, &g);
 14         printf("BSS = 0x%X\n", &t);
 15         printf("HEAP = 0x%X\n", p);
 16         printf("STACK = 0x%X\n", &q);
 17         printf("========\n");
 18         sleep(1);
 19         g++;
 20     }
 21
 22     free(p);
 23     return 0;
 24 }

DATA와 BSS 주소가 바로 옆인 걸 확인 가능 -> 전역 변수 메모리는 서로 붙어 있다.

 

즉, 각각의 프로세스는 독립적인 메모리 공간을 가지고 있다.

 

스레드 (Thread)

프로세스 내에서 실제로 작업을 수행하는 주체를 의미하며, 실행되는 흐름의 단위

https://en.wikipedia.org/wiki/Thread_(computing)

일반적으로 하나의 프로세스는 하나의 스레드를 가짐


CPU

CPU는 한 번에 한 가지 동작만 수행 --> 이외 다른 프로세스는 멈춰있는 상태

동시에 여러 프로세스가 동시에 수행되려면, 굉장히 빠르게 하나씩 돌아가면서 수행해야 한다

--> 이런 작업을 커널이 지원

 

한 프로세스에서 하나의 캐릭터가 뛰어놀도록 개발은 쉽다

한 프로세스에서 여러 캐릭터들이 동시에 움직이도록 코딩하기는 어려움 : 동시 작업 프로그래밍
--> "운영체제"가 API로 지원해주어야 함 (Thread를 위한 POSIX API가 존재)

따라서, 임베디드 제품 개발 시
    - 여러 프로세스를 동시에 동작시키는 멀티태스킹을 구현해야 할 때
    - 한 프로세스 내, 쓰레드 프로그래밍이 필요할 때

Firmware(직접 개발) 보다, POSIX API가 지원되는 RTOS or Linux 급 OS를 *embedded 제품에 설치 후 개발 진행
(Firmware로도 어렵게 개발은 가능)

*Porting : 실행 가능한 프로그램이 원래 설계된 바와 다른 컴퓨팅 환경에서 동작할 수 있도록 하는 과정

 


Void *

어떤 Type의 주소도 모두 다 저장할 수 있는 pointer

단, 주소를 저장만 가능하고, 일반 pointer처럼 바로 사용할 수는 없음

저장은 가능, 값 출력은 불가

그래서 변경해서 사용한다.

#include <stdio.h>

int main(){
	int x = 0;
	char t = 'A';

	void* p1 = &x;
	void* p2 = &t;

	int* p = (int*)p1;  // void* p1을 int*로 변환 후, int* 변수에 저장
	printf("%d", *p);  // 그러면 출력이 가능
    
	printf("%d", *(int *)p1);  // 이 방법도 가능
	
	return 0;
}

 


Thread Programming

두 개 이상의 함수를 동시에 실행시키고 싶을 때 사용

 

다음 코드를 보면,

  1 #include <stdio.h>
  2 #include <unistd.h>
  3
  4 void abc(){
  5     while(1){
  6         printf("ABC\n");
  7         sleep(1);
  8     }
  9 }
 10
 11 void bts(){
 12     while(1){
 13         printf("BTS\n");
 14         sleep(1);
 15     }
 16 }
 17
 18 int main(){
 19
 20     abc();
 21     bts();
 22
 23     return 0;
 24 }

abc() 만 계속 수행될 것임을 알 수 있음

 

이번엔 이 코드를 보면

  1 #include <stdio.h>
  2 #include <pthread.h>  // POSIX 스레드 라이브러리
  3 #include <unistd.h>
  4
  5 void *abc(){
  6     while(1){
  7         printf("ABC\n");
  8         sleep(1);
  9     }
 10
 11     return 0;
 12 }
 13
 14 void *bts(){
 15     while(1){
 16         printf("BTS\n");
 17         sleep(1);
 18     }
 19
 20     return 0;
 21 }
 22
 23 int main(){
 24
 25     pthread_t t1, t2;  // 스레드 구조체 변수 선언
 26
 27     pthread_create(&t1, NULL, abc, NULL);
 28     pthread_create(&t2, NULL, bts, NULL);
 29
 30     pthread_join(t1, NULL);
 31     pthread_join(t2, NULL);
 32
 33     return 0;
 34 }
$ gcc ./thread.c -o ./thread -lpthread

어? 근데 작업 순서가 이상하다 -> 동기화 작업 필요 / 이 부분은 나중에

 

하나하나 파헤쳐 보자

pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

pthread_create(&t1, NULL, abc, NULL);

pthread_create() : 새로운 스레드를 생성하고 실행하는 데 사용

    - arg1 : 스레드의 ID가 저장될 변수 주소 / 이 변수에 생성된 스레드의 ID가 저장
    - arg2 : 쓰레드 설정 (attribute) : NULL 은 Default 설정
    - arg3 : 실행할 함수 이름 / void * 타입의 인자를 받고, void * 타입의 값을 반환해야 함
    - arg4 : 함수에 인자 값을 전달해주고 싶을 때 사용

 

int pthread_join(pthread_t thread, void **retval);

pthread_join(t1, NULL);

pthread_join() : 생성된 스레드의 실행이 완료되기를 기다리고, 스레드가 반환하는 값이나 상태를 처리하기 위해 사용

    - arg1 : 종료를 기다리고자 하는 스레드의 식별자

    - arg2 : 스레드가 종료될 때 반환하는 값의 포인터를 저장할 포인터 / 반환 값을 사용하지 않을 경우 NULL

 


구조체 변수 보내기

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <pthread.h>
  4
  5 struct Node{
  6     int y, x;
  7 };
  8
  9 void* abc(void* p){
 10     struct Node *a = (struct Node*) p;  // 방법 1 : 구조체 포인터로 받기
 11     struct Node b = *(struct Node*) p;  // 방법 2 : 구조체로 받기
 12
 13     while(1){
 14         printf("%d %d\n", a->y, a->x);
 15         printf("%d %d\n", b.y, b.x);
 16         sleep(1);
 17     }
 18 }
 19
 20 int main(){
 21     pthread_t tid;
 22     struct Node gv = {1, 2};
 23
 24     pthread_create(&tid, NULL, abc, &gv);
 25     pthread_join(tid, NULL);
 26
 27     return 0;
 28 }

 

값 예측해보기

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <pthread.h>
  4
  5 pthread_t tid[4];
  6
  7 void* run(void* arg){
  8
  9     int a = *(int*)arg;
 10     printf("%d\n", a);
 11 }
 12
 13 int main(){
 14
 15     for (int i=0; i<4; i++){
 16         pthread_create(&tid[i], NULL, run, &i);
 17     }
 18     for (int i=0; i<4; i++) pthread_join(tid[i], NULL);
 19
 20     return 0;
 21 }
 
 // 출력 결과는?
 thread가 준비하는 동안 main은 계속 진행된다.
 주소에 ++가 4번 적용된 값이 있으므로, 메모리 공유 문제로 전부 4가 나온다.

 


Thread 동기화

용어 정리

1. Race Condition
    - Thread Process 의 타이밍에 따라 결과 값이 달라질 수 있는 상태
2. Critical Section
    - Thread / Process 가 동시에 접근하면 안되는 곳
    - HW 장치를 사용하는 곳 / Shared Resource 전역 변수 등

 

다음 코드를 실행해보자

  1 #include <stdio.h>
  2 #include <pthread.h>
  3 #include <stdlib.h>
  4
  5
  6
  7 pthread_t tid[4];
  8 int cnt;
  9
 10 void* run()
 11 {
 12     for (int i = 0; i<100000000; i++) cnt++;
 13 }
 14
 15 int main()
 16 {
 17
 18     for (int i = 0; i<4; i++) pthread_create(&tid[i], NULL, run, &i);
 19     for (int i = 0; i<4; i++) pthread_join(tid[i], NULL);
 20
 21     printf("%d\n", cnt);
 22
 23     return 0;
 24 }

출력값이 400,000,000이 나와야 하는데, 400,000,000이 나오지 않는 경우가 많다.

빈복문 계산 과정을 보면,

 

1. 메모리에서 레지스터로 변수 값이 등록
2. PC(Program Counter)가 덧셈 명령을 받아 레지스터 값이 ALU에 의해 계산되어 업데이트
3. 계산된 값이 다시 메모리에 저장 

 

이 과정이 멀티 스레드 환경에서 중첩되어 일어난다면 덧셈 계산이 제대로 되지 않는다.

 

 


Thread / Process Synchronization

Critical Section을 동시에 수행하지 않도록 않게 하기 위해 Thread 간 협의를 맞추는 것

동기화 알고리즘 종류 : Mutex, 세마포어, Spin Lock, Barrier 등이 있다.

모두 pthread 라이브러리가 지원

 

1. sleep 활용

아주 작은 sleep을 넣어서 타이밍을 바꿈

  1 #include <stdio.h>
  2 #include <pthread.h>
  3 #include <unistd.h>
  4
  5
  6 pthread_t tid[4];
  7 int cnt;
  8
  9 void* run()
 10 {
 11     for (int i = 0; i<100000000; i++) cnt++;
 12 }
 13
 14 int main()
 15 {
 16
 17     for (int i = 0; i<4; i++) {
 18         pthread_create(&tid[i], NULL, run, NULL);
 19         usleep(100);
 20     }
 21
 22
 23     for (int i = 0; i<4; i++) pthread_join(tid[i], NULL);
 24
 25     printf("%d\n", cnt);
 26
 27     return 0;
 28 }

유지보수 측면에서는 좋지 않음

 

2. Mutex_lock 객체 사용

mutex_lock 객체를 화장실 열쇠라고 생각하면 이해하기 쉬움

간단 사용법

pthread_mutex_init
( &mutex, attr )
    - mutex 객체 초기화
    - attr 에 NULL 을 넣으면 기본 값으로 처리

pthread_mutex_destroy
    - 객체 제거 (조심해서 사용)

pthread_mutex_lock
    - mutex lock 을 요청하여 얻음
    - 얻을 수 있을 때 까지 block 됨
  1 #include <stdio.h>
  2 #include <pthread.h>
  3 #include <unistd.h>
  4
  5
  6 pthread_mutex_t mlock;
  7 pthread_t tid[4];
  8 int cnt;
  9
 10 void* run()
 11 {
 12     pthread_mutex_lock(&mlock);  // mutex lock을 요청하여 얻음 
 13     for (int i = 0; i<100000000; i++) cnt++;
 14     pthread_mutex_unlock(&mlock);  // mutex lock을 반환
 15 }
 16
 17 int main()
 18 {
 19
 20     pthread_mutex_init(&mlock, NULL);  // mutex 객체 초기화
 21     for (int i = 0; i<4; i++) {
 22         pthread_create(&tid[i], NULL, run, NULL);
 23         usleep(100);
 24     }
 25
 26
 27     for (int i = 0; i<4; i++) pthread_join(tid[i], NULL);
 28
 29     printf("%d\n", cnt);
 30
 31     return 0;
 32 }

값이 잘 나오는 걸 확인할 수 있음