선행 지식
간단한 개념에 대해서만 설명하며 자세한 내용은 아래에서 추가적으로 다룬다.
프로세스와 스레드
•
프로그램(Program) : 실행할 수 있는 파일
•
프로세스(Process) : 프로그램이 실행되어 메모리에 탑재된 인스턴스 (독립 개체)
•
스레드(Thread) : 프로세스 내에서 실행되고 있는 여러 흐름 단위(stack을 제외한 영역 공유)
데이터 레이스(Data Race)
멀티 스레드/프로세스 환경에서 공유 자원에 동시 접근할 때 일어나는 경쟁 상황을 일컫는다.
데이터 레이스가 발생하면 의도치 않은 동작이 발생할 수 있으며 이를 방지하기 위해서는 공유 자원에 대한 접근을 제한적으로 허용하는 상호배제가 필요하다.
해결 방법1 (with philosopher)
뮤텍스 및 세마포어는 비동기 안전 함수가 아니라 핸들러에서는 사용이 불가능하다.
pthread_mutex_t lock = 0;
pthread_mutex_lock(&lock);
// something
pthread_mutex_unlock(&lock);
C
복사
해결 방법2 (with minitalk)
시그널 핸들러에서는 volatile sig_atoimic_t를 사용해야 한다.
volatile sig_atomic_t lock = 0;
lock = 1;
// something
lock = 0;
C
복사
원자성(atomicity)
어떤 객체에 대한 연산은 분리될 수 없는 인터럽트 되지 않는 과정으로 이루어져야 되다는 개념이다.
•
공유 변수를 읽는 도중 다른 스레드에서 이 변수를 읽으면 데이터 레이스가 발생한다.
•
이를 방지하기 위해 읽기 및 쓰기에 대해 원장성을 보장하는 타입이나 함수 등이 존재하나 자세한 내용은 아래에서 추가적으로 다룬다.
비원자 접근 오류 예시
•
아래의 코드는 0 0 0 또는 1 1 1이 출력되어야 할 것 같지만 데이터 할당 중 시그널이 호출되면 그 외의 값이 출력 될 수 있다. ‘
•
printf는 비동기 안전 함수가 아니기 때문에 원래는 핸들러에서 호출하면 안되지만 main에서 사용하지 않고 핸들러에서만 호출하기 때문에 이 경우에는 사용이 가능하다.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
struct two_words { int a, b, c; } memory;
void handler(int signum) { printf ("%d %d %d\n", memory.a, memory.b, memory.c); }
void main (void)
{
static struct two_words zeros = { 0, 0, 0 }, ones = { 1, 1, 1 };
signal (SIGUSR1, handler);
memory = zeros;
printf("pid : %d\n", getpid());
while (1) {
memory = zeros;
memory = ones;
}
}
C
복사
$ while true; do kill -USR1 4242; done; # 테스트
Shell
복사
휘발성(volatility)
레지스터에 공유 메모리의 값을 버퍼링하는 GCC의 최적화를 막기 위해 공유 메모리 상의 모든 객체는 volatile 속성의 타입으로 선언되어야 한다.
•
일반적으로 스택에 할당하는 지역 변수는 공유하지 않으므로, 서로 공유되는 전역 변수의 경우에만 필요에 따라 volatile을 사용하면 된다.
•
volatile은 변수 값이 비동기적으로 변동될 수 있음을 컴파일러에게 알려 GCC 최적화를 수행하지 않고 항상 메모리에 접근하게 할 수 있다.
주로 사용되는 환경
1.
멀티 쓰레드 환경
2.
메모리 맵 입출력(MIMO; Memory-mapped I/O)
3.
인터럽트 서비스 루틴(ISR; Interrupt Service Routine)
아래의 코드가 메모리 주소에 연결된 하드웨어에 특정 명령을 전달하는 임베디드 프로그램인 경우 하드웨어 오동작을 일으킬 수 있다.
*(unsigned int *)0x8C0F = 0x8001; // 컴파일러에 의해 최적화되어 사라짐
*(unsigned int *)0x8C0F = 0x8002; // 컴파일러에 의해 최적화되어 사라짐
*(unsigned int *)0x8C0F = 0x8003; // 컴파일러에 의해 최적화되어 사라짐
*(unsigned int *)0x8C0F = 0x8004; // 컴파일러에 의해 최적화되어 사라짐
*(unsigned int *)0x8C0F = 0x8005; // 마지막 연산만 수행됨
C
복사
*(volatile unsigned int *)0x8C0F = 0x8001; // 정상 동작
*(volatile unsigned int *)0x8C0F = 0x8002; // 정상 동작
*(volatile unsigned int *)0x8C0F = 0x8003; // 정상 동작
*(volatile unsigned int *)0x8C0F = 0x8004; // 정상 동작
*(volatile unsigned int *)0x8C0F = 0x8005; // 정상 동작
C
복사
블록과 동기
블록킹 & 논블록킹
프로세스 유휴 상태에 대한 매커니즘 (함수 실행에 대한 제어권을 넘겨주느냐 차이)
•
블록킹(Blocking): 제어권을 넘긴 다른 주체가 끝날 때 까지 기다리는 방식
•
논블록킹(Non-Blocking): 제어권을 넘기지 않고 다른 주체와 관련없이 자신의 작업을 하는 방식
동기 & 비동기
프로세스 수행 순서에 대한 매커니즘 (작업을 완료했는지 반환 값 확인 여부)
•
동기(Synchronous) : 다른 주체의 반환 값을 계속 확인하는 방식
•
비동기(Asynchronous) : 다른 주체의 반환 값을 신경쓰지 않는 방식
동기&비동기 + 블록킹&논블록킹 조합
조합 | 커피 주문 예시 | 시그널 핸들러에서의 예시 | 활용 예시 |
Async-Blocking | 호출벨이 울릴 때 까지 대기 | ||
Sync-Blocking | 커피가 나올 때 까지 대기 | 시그널을 블록하여 핸들러가 종료될 때 까지 일반 코드는 대기한다. | 커맨드 입력 대기 |
Async-NonBlocking | 다른 일 하다가 호출벨이 울리면 받으러가기 | 일반 코드를 반복하다가 시그널이 오면 핸들러를 실행한다. | AJAX 요청 / JS 비동기 콜백 |
Sync-NonBlocking | 다른 일 하면서 대기번호 계속 확인 | 일반 코드를 반복하면서 시그널이 왔는지 플래그를 계속 확인한다. | 로딩 화면 출력 |
비트 통신
비트 연산
11 | 10 | 01 | 00 | ||
A & B | 1 | 0 | 0 | 0 | A와 B가 참인가? |
A | B | 1 | 1 | 1 | 0 | A 또는 B가 참인가? |
A ^ B | 0 | 1 | 1 | 0 | A 와 B의 비트가 다른가? |
~A | 0 | 1 | - | - | 비트 반전 |
A << 1 | 10 | 00 | 10 | 00 | 비트를 1만큼 왼쪽으로 시프트 |
A >> 1 | 01 | 01 | 00 | 00 | 비트를 1만큼 오른쪽으로 시프트 |
•
비트 설정 : A |= B
•
비트 제거 : A &= ~B
•
비트 토글 : A ^= B
LSB & MSB
•
LSB(Least Significant Bit) : 최하위 비트
•
MSB(Most Significant Bit) : 최상위 비트
LSB & MSB를 왜 알아야하는가?
•
임베디드 시스템 등에서 직렬 통신을 할 때 송수신 데이터의 각 비트를 단위시간에 따라서 0 또는 1을 순차적으로 보낸다.
•
수신받은 데이터는 우측으로 데이터를 시프트(>>) 하고 MSB에 비트를 받는다.
•
이 때, 송신 측에서 LSB와 MSB 중 무엇을 먼저 보내냐에 따라 데이터의 값이 달라질 수 있으므로 주의해야한다.
◦
LSB First : 리틀 엔디안 방식과 동일
◦
MSB First : 빅 엔디안 방식돠 동일
통신 속도
시간를 재는 단위
값 | 관련 대기 함수 | |
s (second) | sleep() | |
㎳ (milli second) | 없음 | |
㎲ (micro second) | usleep() | |
㎱ (nano second) | nanosleep() | |
㎰ (pico second) | 없음 | |
fs (femto second) | 없음 | |
as (atto second) | 없음 |
Bit Rate / bps(bit per second)
초당 보낼 수 있는 비트(0 또는 1)의 수.
•
ex) 2400bit/second(bps) 라면 초당 2400개의 비트 정보를 전달할 수 있다는 뜻이고,
이는 비트 정보를 보내기 위해서 416.6 ㎲ 의 시간(1s/2400bit)이 필요하다는 뜻이다.
Baud Rate
초당 보낼 수 있는 심볼(의미있는 데이터 묶음)의 수
•
baud는 신호의 크기에 따라 변경된다.
◦
하나의 심볼이 1bit로 구성되어 있는 경우라면 bps = baud 이다.
◦
하나의 심볼이 2bit로 구성되어 있는 경우라면 bps = baud * 2 이다.
◦
하나의 심볼이 8bit로 구성되어 있는 경우라면 bps = baud * 8 이다.
•
ex) 8bit 심볼의 경우 800bps 는 100baud를 의미하고 1초에 100개의 심볼(ASCII)를 전송할 수 있다.
•
ex) 10bit 심볼의 경우 800bps는 80baud를 의미하고 1초에 80개의 심볼을 전송할 수 있다.통시
공용체 Union 활용
구조체와 동일하지만 공용체의 경우는 가장 큰 자료형의 공간을 공유한다.
union u_test
{
int num2;
short num1;
char c1;
};
union u_test test;
test.num2 = 0xFFFFFFFF;
printf("%x\n", test.num2); // ffffffff
test.num1 = 0;
printf("%x\n", test.num2); // ffff0000
C
복사
[참고] 네트워크 패킷 예시
[참고] 최상위, 최하위 접근 예시
[참고] 비트 필드를 통한 비트 접근 예시
패리티 비트(Parity Bit)
직렬 통신에서 데이터의 오류를 검출하기 위한 방법으로, 전송하고자 하는 데이터의 각 문자에 1비트를 더하여 전송한다.
•
두 개의 비트가 손실 된 경우는 알아차릴 수 없으나, 하나의 비트에 대한 오류는 검출할 수 있다.
•
짝수 패리티 : 프레임에 포함된 1의 개수가 짝수가 되도록 비트 구성
•
홀수 패리티 : 프레임에 포함된 1의 개수가 홀수가 되도록 비트 구성
해밍 코드(Hamming code)
오류 검출만 가능한 패리티 비트 대신 오류 정정까지 가능한 해밍코드를 사용해보는 것도 좋다.
2Bit 의 오류를 검출할 수 있고, 1Bit 의 오류를 교정할 수 있지만, 오류 검출 및 교정을 위한 잉여 비트가 많이 필요하다.
•
해밍코드 생성 규칙
◦
1,2,4, …, 번째 자릿수를 패리티 비트로 둔다.
◦
이 숫자로부터 시작하는 3개의 패리티 비트가 짝수인지, 홀수인지 기준으로 판별한다.
◦
n 번째 패리티 비트는 n 비트에서 시작하여, n 비트 만큼을 포함하고, n 비트 씩 건너뛴 비트 들을 대상으로, 패리티 비트를 결정한다.
•
추가할 패리티 비트 개수는 아래 공식을 만족해야 한다.
◦
2^p ≥ d + p + 1 (ex, 2^3 ≥ 4 + 3 + 1 )
▪
d = 데이터 비트 수
▪
p = 패리티 비트 수
[참고] 해밍코드 이해를 돕기 위한 예제
•
1 0 1 1 0 0 1 1의 해밍 코드를 받은 경우 원본 데이터는 번째를 제외한 1 0 0 1 이다.
•
1 0 1 1를 홀수 해밍 코드로 변환한다고 가정하는 경우
◦
패리티 비트는 2^n 에 위치해야므로, ? ? 1 ? 0 1 1이다.
◦
패리티 규칙이 홀수로 정해졌으므로, 각 자리의 패리티 비트를 결정할 수 있다.
▪
1(2^0) 번째는, 1번째부터 1개 포함 1개 스킵인 1,3,5,7번째 값인 ? 1 0 1대상이므로 1이 된다
▪
2(2^1) 번째는, 2번째부터 2개 포함 2개 스킵인 2,3,6,7번째 값인 ? 1 1 1대상이므로 0이 된다.
▪
4(2^2) 번째는, 4번째부터 4개 포함 4개 스킵인 4,5,6,7번째 값인 ? 0 0 1대상이므로 1이 된다
◦
따라서, 변환된 해밍 코드는 '1 0 1 1 0 1 1' 이다.
•
짝수 패리티 비트를 적용한 해밍 코드가 0 0 1 1 0 1 1 일 때, 오류 수정
◦
1, 3, 5, 7 번째 비트 확인
▪
'0 1 0 1' 합이 짝수이며, 짝수 패리티 비트와 일치하므로 '0'
◦
2, 3, 6, 7 번째 비트 확인
▪
'0 1 1 1' 합이 홀수이며, 짝수 패리티 비트와 일치하지 않으므로 '1'
◦
4, 5, 6, 7 번째 비트 확인
▪
'1 0 1 1' 합이 홀수이며, 짝수 패리티 비트와 일치하지 않으므로 '1'
◦
이후, 역순으로 패리티비트 '110' 을 도출할 수 있다.
◦
이제, 2진법 '110' 을 10진법 '6' 으로 바꾼 뒤, 6 번째 비트를 수정하면 된다.
◦
따라서, 정답은 0 0 1 1 0 '0' 1 이 된다.
유니코드
전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 산업 표준이다.
유니코드 구조
•
첫번째 바이트의 시작 비트를 통해 몇 바이트인지 확인 가능하다. (하지만 과제할 때는 몰라도 되는 정보다…)
바이트 수 | 비트 구조 |
1 바이트 | 0xxxxxxx |
2 바이트 | 110xxxxx 10xxxxxx |
3 바이트 | 1110xxxx 10xxxxxx 10xxxxxx |
4 바이트 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
유니코드 출력
•
write() 할 때 터미널에서 알아서 표시하므로 1바이트 씩 출력해도 문자열이 깨지지 않고 출력 된다.
#include <stdio.h>
int main(int argc, char **argv)
{
for (int i = 0; argv[1][i]; i++)
printf("%c\n", argv[1][i]);
printf("return : ");
for (int i = 0; argv[1][i]; i++)
printf("%c", argv[1][i]);
printf("\n");
return (0);
}
C
복사
$ cc test.c; ./a.out 안녕
� # '안'의 1번째 바이트
� # '안'의 2번째 바이트
� # '안'의 3번째 바이트
� # '녕'의 1번째 바이트
� # '녕'의 2번째 바이트
� # '녕'의 3번째 바이트
return : 안녕
Shell
복사