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
|
SRAM
|
|
접근 속도 | 느림 | 빠름 |
사용 용도 | 주 기억 장치 | 캐시 메모리 |
집적도 | 높음 | 낮음 |
소비전력 | 적다 | 많다 |
프로그램 (Program) vs 프로세스 (Process) vs 프로세서 (Processor>
1. 프로그램 : CPU의 2진수 명령어들의 집합
2. 프로세스 : 프로그램을 실행(메모리에 로드)한 결과
3. 프로세서 : 프로그램의 명령을 실행하는 컴퓨터의 하드웨어
메모리의 역할
1. 0과 1로 된 CPU 명령어를 저장하는 역할
2. 변수가 만들어지는 공간
.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 }
즉, 각각의 프로세스는 독립적인 메모리 공간을 가지고 있다.
스레드 (Thread)
프로세스 내에서 실제로 작업을 수행하는 주체를 의미하며, 실행되는 흐름의 단위
일반적으로 하나의 프로세스는 하나의 스레드를 가짐
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 }
빈복문 계산 과정을 보면,
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 }