dup, dup2 이외에도 파일을 다루는 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

파일 서술자 복제 함수 dup, dup2

함수 이름에서도 알 수 있듯이 무엇을 복제한다는 함수입니다. 무엇을 복제할까요? 바로 파일 서술자(file descriptor)입니다. 함수 이름이 너무 심플하네요. 이 함수들을 사용하기 위해서는 <unistd.h>를 꼭 include해야합니다.

 

dup

#include <unistd.h>
int dup(int fd);

dup는 fd로 전달받은 파일 서술자를 복제하여 반환합니다. dup가 돌려주는 파일 서술자는 가장 낮은 서술자를 반환합니다. 성공시 새 파일 서술자, 오류시 -1을 반환합니다.

 

dup2 

#include <unistd.h>
int dup2(int fd, int fd2);

dup2는 새 서술자의 값을 fd2로 지정합니다. 만일 fd2가 이미 열려있으면 fd2를 닫은 후 복제가 됩니다. 역시 성공시 새 파일 서술자, 오류시 -1을 반환합니다.

 

dup 예제

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>

int main(void){
        int fd1, fd2;
        char message[32]={"message via fd2\n"};

        fd1=open("made_by_fd1",O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd1<0){
                printf("file open error\n");
                exit(0);
        }


        fd2=dup(fd1);

        write(fd2,message,strlen(message));
        printf("fd1 :%d, fd2:%d\n",fd1,fd2);
        close(fd1);
        close(fd2);

}

 

파일 디스크립터 0, 1, 2는 이미 표준 입출력에 의해서 할당되어 있는 상태입니다. 

위의 코드는 made_by_fd1이라는 파일을 열고 그 파일 디스크립터가 fd1에 할당됩니다. 그 후 fd1을 복제하여 나온 파일디스크립터를 fd2가 갖고 있게 됩니다. 

그렇다면 write하여 fd2에 쓰면 어떻게 될까요? 

아래의 그림을 참고하여 예측해봅시다.

 

예상하셨나요? fd2는 fd1 파일 서술자를 복제하였으니 made_by_fd1에 메시지를 기록하게 됩니다. 

다음은 그 결과입니다. dup은 남아있는 파일 서술자 중 가장 작은 값을 내어주니 fd2가 4인것도 확인할 수 있네요.

 

# gcc dup_test.c
# ./a.out
fd1 :3, fd2:4
# cat made_by_fd1
message via fd2

 

dup2 예제

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>

int main(void){
        int fd1, ret;
        char message[32]={"STDERR from fd1\n"};

        //그림 1번
        fd1=open("made_by_fd1",O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd1<0){
                printf("file open error\n");
                exit(0);
        }
        //표준 입출력으로 print됨
        printf("file open\n");

        //fd1의 파일 디스크립터가 명시한 STDOUT_FILENO의 파일 디스크립터로
        //복제됨,
        //그림 2번
        ret=dup2(fd1,STDOUT_FILENO);

        //fd1으로 출력됨
        printf("fd1 :%d, ret:%d\n",fd1,ret);

        //STDERR_FILENO 디스크립터가 명시된 fd1으로 복제됨
        //그림 3번
        ret=dup2(STDERR_FILENO,fd1);

        //fd1은 에러로 출력됨
        write(fd1,message,strlen(message));

        //stdout이 file로 써짐
        printf("printf를 썼지만 파일에 기록됨 \n");

        close(fd1);

}

 

글로만 설명하는 것보다 이해하기 쉽도록 그림과 같이 설명해보도록 하겠습니다. 

우선 여러분은 위 코드를 보고 왜 아래의 그림이 되는지 이해해보도록 해봅시다.

 

 

1. 우선 파일을 여는데 위의 예제와 같은 이름으로 파일을 엽니다. 그렇다면 fd1은 3이 되겠네요. 

 

2. dup2로 STDOUT_FILENO라는 파일 서술자를 명시된 fd1로 바꿔버립니다. dup2를 조금 더 쉽게 이해하려면 두 번째인자가 첫 번째 인자로 가리키는 화살표 방향이 바뀐다라고 이해하시면 됩니다. 그리고 dup2는 성공적으로 호출이 되면 두 번째 인자의 값을 반환합니다. 실패시 -1을 반환하므로 에러 처리는 필수인데 저는 귀찮아서 하지 않았습니다.

 

printf는 표준 출력인데 printf로 문자열을 출력한다면 fd1으로 출력하는 것과 같습니다. 그러니까 우리가 만든 파일로 출력이 되겠네요.

 

3. 이제 fd1을 표준 에러로 redirect합니다. 방향을 바꿔버린다는 것이죠. 그렇게 되어 fd1으로 메시지를 출력하게 되면 표준 에러로 메시지를 출력하는 것과 같습니다. 

 

이제 결과를 보도록할까요?

# gcc dup2_test.c
# ./a.out
file open
STDERR from fd1
# cat made_by_fd1
fd1 :3, ret:1
printf를 썼지만 파일에 기록됨

이상으로 dup, dup2에 대한 설명과 사용법을 예제를 통해 알아보았습니다. 이해하기 쉽도록 설명하려고 노력했는데 이해하셨는지 모르겠네요.

 

감사합니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

HMAC(Hashed MAC)

HMAC은 CMAC과 더불어 자주 쓰이는 메시지 인증 코드 생성 방법입니다. 메시지 인증 코드(MAC : Message Authentication Code)에 대해서는 지난 포스팅을 참고하시기 바랍니다.

 

HMAC은 2번의 Hash 함수와 1개의 대칭키(또는 비밀키라고 하지요)를 사용합니다. 대칭키를 사용하였으니 메시지의 출처를 인증할 수 있겠네요. 왜냐면 key는 단 둘만 알고 있는 비밀이기 때문입니다. 그러니 메시지의 인증이 가능한 메시지 인증 코드입니다. 

 

HMAC의 과정은 아래의 그림이 간략하게 보여줍니다. 

 

 

이 그림에 ipad인지 opad인지 모르는 녀석들도 섞여있네요. 이제 글과 함께 위의 그림을 같이보도록 합시다. 

 

 

HMAC의 크기: n

메시지 블록의 크기: m 

 

1. 메시지 블록의 크기를 나눈다

우선 메시지를 일정한 크기(256 bit(32 byte)이던 512 bit(64 byte)이던)로 나눕니다. 이 일정한 메시지 블록의 크기를 m이라고 합시다. 

 

2. 비밀키를 패딩으로 채운다.

비밀키는 최종적으로 나오는 HMAC, 즉 n의 크기 이상을 권장합니다. 만약 비밀키가 m비트 이하라면 왼쪽에 0을 덧붙여 비밀키를 m비트의 크기로 채웁니다.

 

3. 그렇게 나온 비밀키와 ipad(input pad)와 XOR 연산을 한다. 

ipad는 m/8번의 연속된 이진열로 0011 0110(0x36)으로 이루어져있습니다. 자, 그럼 여기서 m은 8의 배수로 이루어져있어야 m/8이 정수가 되겠네요. 만약 m이 16비트라면 ipad는 0011 0110 0011 0110인 ipad가 될것이고 패딩된 키와 XOR 연산이 될 것입니다.

 

4. 3의 연산된 값을 메시지 블록 맨 앞에 놓는다.

이렇게 되면 총 N+1개의 메시지 블록이 생겨납니다.

 

5. 해쉬 함수로 메시지 다이제스트를 n비트 생성한다.

4에서 연산된 N+1개의 메시지 블록 전체를 해쉬 함수로 해쉬 값을 생성해냅니다. 생성된 값은 중간 HMAC이라고 합니다.

 

6. 중간 HMAC을 m비트로 만들기 위해 0으로 채운다.

이 중간 HMAC을 다시 해쉬 함수를 만들기 위해 m비트로 맞춥니다.

 

7. 다시 패딩된 비밀키와 opad(output pad)를 XOR한다.

opad는 m/8번의 연속된 이진열 0101 1100(0x5C)로 이루어져있습니다.

 

8. 7의 결과값을 중간 HMAC 앞에 놓는다.

최종적인 HMAC을 만들기 위해서 7번의 결과와 HMAC을 합칩니다.

 

9. 마지막으로 중간 HMAC을 만든 동일한 해쉬 함수를 통해 최종 HMAC을 생성해낸다.

 

10. 결과적으로 나온 HMAC을 메시지 뒤에 붙여 상대방에게 전송하면 HMAC의 과정은 끝이납니다.

 

이상으로 HMAC에 대한 설명과 어떻게 만들어지는지에 대한 과정 설명을 마칩니다.

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

 

 

흔히 우리가 문서가 변조 또는 위조되었는지 확인하기 위해서 핑거프린트(finger frint)를 사용하지요. 디지털에서는 어떻게 위조되었는지 알 수 있을까요?

디지털 메시지를 다룰때 우리는 그 메시지가 변경이 되었는지 또는 이 메시지가 내가 알고 있는 그 사람에게 왔는지 확인해야할 필요가 있는데 메시지 변경 감지 코드(MDC:Modification Detection Code)메시지 인증 코드(Message Authentication Code)가 그 역할을 합니다.

 

MDC(Modification Detection Code)

메시지 변경 감지 코드는 메시지의 변경이 일어났는지 아닌지를 확인합니다. 즉, 데이터의 무결성을 판단하게 됩니다.

 

송신자 : 송신자는 원본의 메시지는 그대로 보내고 그 메시지를 암호학적 해쉬 함수를 통해 해쉬값을 만든게 MDC인데, 안전한 채널로 보내게 됩니다. 이때 안전한 채널로 외부로부터 변경이 되지 않는다는 보장이 되어야합니다.


암호학적 해쉬 함수 : 암호학적 해쉬 함수는 메시지의 해쉬 값을 만드는 함수로 일방향 함수여야합니다. 그러니까 해쉬값을 만드는 것은 되지만 해쉬값으로부터 원본 값을 알 수 없어야하고 메시지와 해쉬값이 1:1이 되어야합니다. 암호학적 해쉬함수의 기준은 3가지 저항성을 충족시켜야합니다. 
1) preimage resistance
2) second preimage resistance
3) collision resistance

한마디로 깨기 어려워야한다는 것이고, 저도 암호학적 해쉬함수에 해박한 지식은 없으므로 이정도하고 넘어갑시다.

수신자 : 수신자는 전달받은 메시지를 MDC로 다시 만들며 전달받은 MDC와 비교하여 이 둘이 같다면 변경이 되지 않은 것이고 변경이 되었다면 이 메시지는 변경되었다고 판단합니다.

 

MAC(Message Authentication Code), TCP/IP 2계층 아님.

메시지가 내가 원하는 바로 그 사람으로부터 왔는지 판단하려면 어떻게 하면 좋을까요? 서로만 아는 사실 하나만 추가하면 됩니다. 바로 키만 섞어주면 되죠.

 

송신자 : 송신자는 수신자와 미리 공유된 키를 가지고 메시지 해쉬값을 만들어 냅니다. 이것이 우리가 이야기하는 메시지 인증 코드, MAC입니다. 이것을 메시지와 함께 보냅니다.

수신자 : 송신자로부터 받은 MAC과 메시지를 키와 함께 해쉬값을 만든 후 비교합니다. 같다면 원하는 송신자로 온것이 확인이 되고 아니라면 메시지가 변경이 되었거나, 누군가가 임의로 보낸거겠죠? 

 

 

MAC은 이처럼 무결성과 인증을 같이 제공합니다. 

CMAC

메시지는 일반적으로 큰 경우가 많기 때문에 이 메시지를 한번에 MAC으로 만들 수는 없습니다. 그래서 메시지를 일정 비트 단위로 쪼개서 MAC을 만들어가는데 그 중 한가지가 CMAC입니다.

CMAC은 CBCMAC과 같은 의미로 불리는데 블록 암호에서 CBC와 유사한 방법을 사용하기 때문입니다.  CMAC을 진행하기 전에 송신자와 수신자는 key를 공유한 상황이어야합니다.

 

CMAC은 다음의 과정을 거치게 됩니다. 

 

1) 메시지를 m bit씩 n개로 쪼갭니다.

2) 만약 마지막 메시지 블록이 m bit가 못된다면 나머지 첫비트는 1, 다른 비트는 전부 0으로 채워버립니다. 패딩이라고 하지요.

3) 이제 앞의 메시지 블록부터 차례대로 사전에 공유된 키, key로 암호화하고 암호화한 값을 이 후의 블록과 xor을 진행합니다.

이 과정을 마지막 블록까지 진행합니다.

4) 마지막 블록에는  k와 xor 연산을 진행한 후 마지막 암호화를 진행합니다.  k는 마지막에 한번만 xor연산을 위해 사용되는데 key로부터 파생된 값입니다. m bit의 메시지 블록을 xor하기 위해서는 k도 m bit의 크기를 갖고 있어야겠지요.

5) 그 결과를 왼쪽의 n비트를 뽑아서 메시지에 붙여서 보냅니다.

 

 

 

이상으로 메시지 변경 감지 코드, 메시지 인증 코드, 메시지 인증 코드의 CMAC 방식을 알아보았습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

비대칭키 암호(Asymmetric Key Cryptography)

대칭키 암호가 양쪽 통신 당사자가 같은 키를 공유하는 암호화 방식이라면 비대칭 키는 대충 느낌이 오시지 않나요? 비대칭 키 암호는 양쪽이 다른 키를 가지고 있습니다.

 

비대칭키에서 타인 모두에게 공개하는 키를 공개키(public key), 그리고 자기 자신만이 갖고 있는 키가 개인키(private key)라고 합니다. 따라서 공개키는 누구나 다 알 수 있고 누구나 다 암호화할 수 있지만 푸는 사람은 그에 맞는 개인키를 가지고 있는 사용자가 됩니다.

 

기본 아이디어

아이디어는 다음의 그림과 같습니다.

은밀한 데이터를 보내는 A는 B의 공개키로 데이터를 암호화한 후 B에게 보냅니다. B는 이 데이터를 풀어야하는데 개인키를 통해서 암호화된 데이터를 풀게 되지요. 

 

여기서 비대칭키 암호의 특징 몇가지가 있습니다.

1) public key는 모두에게 공개해야하는데 이 public key를 가지고 private key를 구해내기가 상당히 어려워야합니다. 

2) public key와 private key는 짝이 맞아야합니다. 그래야지 암복호화를 정상적으로 할 수 있게되죠.

3) 대칭키는 통신 주체들이 각각 키를 갖고 있어야하므로 당사자가 n명이 있다고 할때 필요한 키의 개수는 n(n-1)/2개가 됩니다. 하지만 비대칭키 암호는 2개만 있으면 됩니다. 바로 공개키와 개인키가 그것이 되겠죠.

4) 비대칭키의 암호화 과정과 복호화 과정은 상당히 느립니다. 그렇기 때문에 보통의 데이터를 암호화할 수 없죠. 아주 중요한 데이터를 암호화할때 쓰이는데 바로 대칭키의 키 교환할때 쓰입니다.

 

RSA(Rivest, Shamir, Adleman)

비대칭키 암호의 가장 대표적인 알고리즘은 바로 RSA 알고리즘입니다. RSA는 고안자들의 앞 약자입니다. 그 중에서도 Rivest형이 가장 실세인가 보네요.

 

RSA는 소인수 분해의 어려움을 기반으로 하는 알고리즘인데 수학을 좀 많이 알아야만 제대로 이해할 수 있습니다. 저와 같은 일반인 수준에서는 아~~ 그렇게 되는가보다 하고 끝내는게 정신건강에 좋습니다. 여기서는 그냥 아~ 그렇게 되는가 보다 하고 맘편히 읽어주세요.

 

- 오일러 토션 함수(Euler's totiont function)

RSA를 이해하기 전에 우리는 오일러 토션 함수에 대해서 알아야합니다. 저도 모르니 그냥 필요한 식만 적도록 하겠습니다. 함수의 기호를 찾을 수 없으므로(능력 부족..) t라고 표시하도록 하겠습니다.

오일러 토션 함수는 n보다 작은 수 중 n과 서로소인 수의 갯수를 구하는 함수입니다. 서로소라함은 두 수의 공약수가 1외에는 없는 수를 말합니다. 

 

오일러 토션 함수 공식


1. t(1)=0
2. t(p)=p-1, p는 소수
3. t(m*n)=t(m)*t(n)
4. t(p^e)= p^e - p^(e-1) (^는 제곱을 의미합니다. p^e= p의 e제곱)

 

예제)

1. 15보다 작은 수 중에 15와 서로소는 몇개일까요? 

1, 2, 4, 7, 8, 11, 13, 14 로 8개입니다. 우리가 위의 공식을 적용하여 구한다면 아래와 같습니다.

t(15) = t(3*5) = t(3) * t(5) = 2*4 = 8

 

2. t(14)는 얼마일까요? 

t(14) = t(7*2) = t(7) * t(2) = 6*1 =6 , 실제로 1, 3, 5, 9, 11, 13이 있습니다.

 

 

 

 

RSA 암복호화 과정

RSA는 두 소수 p와 q를 선택하여 곱한 n을 구합니다. 이 수는 소수이기 때문에 오일러 토션 함수를 이용해서 t(n) = t(p)*t(q) = (p-1)*(q-1) 이 성립이 됩니다. 이제 이 안에서 두개의 지수 e와 d를 고르는데요. 이 e와 d는 t(n)과 서로소이며 mod t(n) 연산에서 서로 역원 관계입니다. t(n)과 서로소라면 e와 d는 t(n)보다 작은 수 중 소수를 선택하면 무조건 t(n)과 서로소가 되지요.

만약 소수 t(n)보다 작은 e를 선택했다면 (e*d)mod t(n)=1를 만족하는 d를 구하면 됩니다. e는 d의 역원이라하여 e=(d^-1)mod t(n)으로 표현하기도 합니다. 

 

아래에서는 실제 어떻게 RSA를 통해 암복호화가 되는지 살펴보도록 합니다. 최대한 구별가능하게 컬러로 하고 e와 d를 쉽게 구하는 공식도 소개합니다.

 

1) pq를 소수 711로 선택합니다.

2) 그리고 n을 구하죠. n=7*11이므로 77이 됩니다. t(77) = t(7) * t(11) = 60이 됩니다.

3) 이제 ed를 선택하는데 만약 e를 13으로 선택하면 d는 37입니다. (e*d)mod60=(13*37)mod60=1이기 때문에 서로 역원이죠. e는 t(77) = 60보다 작은 수 중 60과 서로소입니다. 13은 소수이기 때문에요.

 

* 여기서 한가지 공식이 있는데, e를 13을 골랐다면 e의 역원은 다음과 같습니다.

(13^(t(77)-1)) mod t(77) = 13^(59) mod 60 = 37

 

번외)

자, 그렇다면 e를 다른 수를 골라보도록 하지요. e를 23을 고른다면 d는 아래와 같이 구해지겠네요.

d = 23^(t(77)-1) mod t(77) = 23^(60-1) mod 60 = 23^59 mod 60 = 47

암호화

이제 평문 5를 RSA로 암호화 한다면 

(5^13)mod 77 = 26이 되어 암호문이 됩니다.

복호화

다시 복호화하게 되면 (26^37)mod 77 = 5가 되므로 복호화가 됩니다. 

여기서 d는 나의 private key로 쓰이고, e는 n과 함께 공개되며 공개키로 쓰입니다.

 

여기서 e와 n을 가지고 d를 구하는것이 아주 상당히 어렵습니다. 수식은 있긴한데 쓰지 않는게 멘탈에 좋을 것 같습니다.

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

대칭키 알고리즘

대칭키 알고리즘이라는 것은 동일한 키를 사용하여 암호화와 복호화를 하는 것을 의미합니다. 기본적인 용어부터 짚어보도록 하지요.

 

평문(Plain text) : 암호 알고리즘이 적용되지 않은 비트열, 또는 문자열을 의미합니다.

암호문(Chiper text) : 암호알고리즘이 적용된 비트열, 또는 문자열입니다. 키를 갖고 있지 않은 사람은 해독할 수 없습니다. 

 

이제 평문이 어떻게 암호문으로 암호화가 되고 어떻게 다시 평문으로 복구가 될까요? 답은 xor연산입니다. xor연산을 2번 적용하게 되 같은 비트열로 돌아오게 되는것을 이용합니다. 아래가 그 예가 되겠네요.

     0100 1100 (plain text)
xor 0110 1100 (key)
------------------------------
     0010 0000 (chiper text)

     0010 0000 (chiper text)
xor 0110 1100 (key)
-------------------------------
     0100 1100 (plain text

 

현재 대칭키 알고리즘은 물론 이것보다 더 많은 복잡한 과정을 더 거치게 됩니다. 

 

그 중에서 가장 많이 사용되는 대칭키 알고리즘인 AES에 대해서 알아보도록 합시다.

 

AES(Advanced Encryption Standard)

평문은 항상 128비트로 고정이 됩니다. 그럼 몇바이트인가요? 16바이트겠네요. 

16바이트의 평문이 Round key와 각 Round에서 지지고 볶아져 다음 Round로 가게 되고 최종 Round에서는 마침내 암호문이 나옵니다.  

 

암호문은 다시 역순서로 복호화되어 평문이 나오게 되지요.

 

AES는 세가지 버전이 있는데, AES128, AES192, AES256이 바로 그것들입니다. 이름뒤에 나오는 숫자는 암호화할때 사용되는 키의 비트수인데, 이 비트수마다 라운드가 다릅니다. 아래의 표로 정리했으니 참고하세요.

Round N Key size
10 128
12 192
14 256

 

- 키확장 (Key Expansion) : 입력으로 들어오는 key를 단순히 쓰는게 아니라 각 라운드에서 사용될 키를 파생시키는 역할을 합니다.

 

그렇다면 각 Round는 어떻게 구성되어있을까요?

우선 암호화될 평문의 데이터(128비트)는 2개의 16진수로 4x4 행렬로 나타냅니다. 이것을 State라고 부르는데 이  4x4 행렬블록은 SubBytes -> Shift Rows -> Mix Columns -> Add Round key를 거쳐서 최종 State가 나오게 되지요.

이렇게 나온 State는 다시 다음 Round로 향하게 됩니다.

 

 

Round 연산을 아래 간략하게 설명합니다.

- SubBytes : 대치(Substitution)을 사용하여 혼돈 효과를 줍니다. AES는 SubBytes 변환 테이블이라는 것을 이용하여 대치합니다.

- Shift Rows : 말 그대로 행을 shift하는데, 비트를 왼쪽으로 이동시킵니다. 코드로는 단순히 row<<1이 되겠네요.

- Mix Columns : 행렬의 곱셈을 이용하여 바이트들을 섞습니다. 행렬에 관한 지식이 있어야합니다. 

State의 열을 특정 C라는 행렬로 새로운 열을 만들어냅니다. 이렇게 column을 뒤섞는데요. 이때 원래의 state로 되돌리기 위해서 복호화할때는 C의 역행렬을 사용해야합니다.
행렬의 연산을 모른다싶으시면 그냥 그런 연산을 하는구나 참고만 하시면 될 것 같습니다.

- Add Round Key : 라운드키를 이용하여 최종적인 State를 만들어냅니다.

 

AES 안전성

AES는 DES 다음의 설계된 대칭키 암호알고리즘으로 어떤 공격도 AES를 뚫는데에 성공하지 못했습니다. 그러므로 현재는 가장 안전한 대칭키 암호알고리즘으로 볼 수 있습니다. 특히 DES와는 다르게 사용하는 키의 크기가 훨씬 큽니다. 

 

AES 구현 

AES는 소프트웨어, 하드웨어, 펌웨어 등으로 구현할 수 있고 매우 빠르게 동작합니다. AES 알고리즘은 단순하여 최소의 메모리를 사용할 수 있습니다. 서로 키만 잘 전달이 된다면 이런 이점을 이용해 AES를 사용해서 안전한 통신을 할 수 있습니다. 

 

이상으로 AES에 대해서 알아보았습니다. 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

동기화, 조건 변수 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

조건 변수

조건 변수를 설명하기 전에 다음과 같은 상황이 발생했다고 칩시다.

먼저 스레드 2개가 존재합니다. 저는 짧은 코드를 좋아하므로 아주 간단한 역할을 하는 2개의 쓰레드를 생성했습니다. 

Thread1 Thread2

data값 +1 증가

값을 출력


이때 thread2는 항상 thread1이 data값을 바꾼 다음에만 출력해야되는 조건이 있다면 이런 상황을 어떻게 구현해야할까요?

 

첫번째 방법은 우선 thread2는 항상 data가 변경되는 것을 지속적으로 감시한 후에 출력하면 되겠죠. 이런 해결 방법을 busy-waiting 또는 spinning이라고 합니다. 바쁜 대기라는 것입니다. 하는 일은 없는데 바쁘게 기다리고 있는 상황입니다. 무한루프로 변경을 감지하게 되는 것이라서 CPU의 점유율을 차지하게 됩니다.

 

그렇지 않고 특정 조건이 발생했을때 signal을 보내서 감지할 수도 있습니다. 이때 사용하는게 바로 조건 변수입니다. 

 

우리는 위와 같은 상황을 코드로 구현하기 전 우리가 사용해야하는 도구들을 먼저 간단히 보도록 하겠습니다.

 

pthread_cond_init

조건 변수를 초기화합니다. 이 함수말고 정적으로 조건 변수를 초기화할 경우에는 PTHREAD_CONT_INITIALIZER 상수를 이용해서 초기화할 수도 있습니다.

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

cond라는 조건 변수를 초기화하는데 attr로 속성을 지정할 수 있습니다. NULL이면 기본 조건 변수를 사용합니다.

 

pthread_cond_wait

조건이 참이 될 때까지 대기하는 함수입니다. pthread_cond_wait의 두번째 인수는 조건 변수를 보호하기 위한 뮤텍스입니다. pthread_cond_wait을 호출하기 전에 전달할 mutex를 이용하여 잠근 후에 이 함수를 호출해야합니다. 즉, pthread_cond_wait전에 pthread_mutex_lock을 호출하는데 둘의 mutex는 같아야한다는 것입니다. 

그러면 이 함수는 호출한 스레드를 조건의 발생을 대기하는 스레드들의 목록에 추가하고 뮤텍스를 풀게됩니다.

int pthread_cond_wait( pthread_cond_t* cond, pthread_mutex_t* mutex );

 

여기서 첫번째 인자가 condition 변수이고, 두번째 인자는 동기화를 할mutex입니다.

 

pthread_cond_signal

대기 중인 스레드에게 signal을 보냅니다. 현재 pthread_cond_wait으로 대기중인 스레드를 깨우게 되어 다른 스레드가 이후의 작업을 진행할 수 있도록 해줍니다. pthread_cond_wait과는 다르게 mutex를 받지 않음을 보세요.

int pthread_cond_signal(pthread_cond_t *cond);

 

이제 이것을 바탕으로 위의 문제점을 해결해보도록 해보지요. 아래의 소스 코드가 그것입니다.

코드 설명 : 아래의 코드는 1초마다 data라는 변수의 값을 증가시키는 thread1과 그 값을 단순히 출력해주는 thread2가 존재합니다. 

thread1은 값을 증가할때마다 thread2에게 출력을 하라고 pthread_cond_signal로 신호를 보냅니다.

data는 공유자원이기 때문에 mutex를 사용했습니다. 여기서 데이터의 조작은 한쪽에서 이루어지긴하지만 공유자원의 조작이 여러곳에서 이루어질 수도 있기 때문에 mutex로 동기화 처리를 하였습니다. 

 

쓰레드 동기화 기법에 대한 설명은 지난 포스팅을 참고하시기 바랍니다.

#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>

pthread_mutex_t mutex;
pthread_cond_t cond;

int data=0;

void *increase(void *arg){
        while(1){
                pthread_mutex_lock(&mutex);
                pthread_cond_signal(&cond);
                data++;
                pthread_mutex_unlock(&mutex);
                sleep(1);
        }
}

void *printData(void *arg){
        while(1){
                pthread_mutex_lock(&mutex);
                pthread_cond_wait(&cond,&mutex);
                printf("data :%d\n",data);
                pthread_mutex_unlock(&mutex);
        }
}
int main()
{
    pthread_t thread1,thread2;

    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_create(&thread1, NULL, increase,NULL);
    pthread_create(&thread2, NULL, printData,NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

 

이제 아래의 명령으로 컴파일 후 실행 결과를 보도록 하겠습니다. 결과를 보게되면 1초마다 값이 증가하는 것을 볼 수 있습니다.

# ./a.out
data :1
data :2
data :3
data :4
data :5

 

생산자-소비자 예제

한가지 예제를 더 살펴보도록 합시다. 생산자-소비자의 예제를 조건변수를 사용하여 구현한 코드입니다.

 

코드 설명:

코드가 길어보이지만 큐(queue)라는 자료구조를 아신다면 코드의 절반은 큐의 구현입니다. 그 중에서도 원형큐죠?

 

producer, 생상자 - item을 생성하여 queue에 집어넣습니다. 아래 produce함수를 실행하는 thread이지요.

consumer, 소비자 - item을 큐에서 꺼내어 출력합니다. consume함수를 실행하는 thread입니다.

공통 -현상을 명확히 보기 위해 양쪽 스레드는 랜덤한 시간을 기다렸다가 생산, 소비하게 됩니다.

 

만약 큐가 비어져있으면 소비할 item이 없으니 생산자에게 아이템을 생산하라는 신호를 보냅니다. c_cond가 바로 이 조건 변수입니다.

만약 큐가 전부 다 찼다면 생상할 수 없으니 소비자에게 소비하라는 신호를 보냅니다. p_cond가 바로 그 조건 변수입니다. 

 

코드는 아래와 같습니다.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

#define Q_MAX       15 + 1

// ================ queue implementation =======================
typedef struct queue_t {
        int             data[Q_MAX];
        int             nitem;
        int             head;
        int             tail;

        pthread_mutex_t mutex;
        pthread_cond_t  c_cond;
        pthread_cond_t  p_cond;

}queue_t;

queue_t queue={
        .p_cond=PTHREAD_COND_INITIALIZER,
        .c_cond=PTHREAD_COND_INITIALIZER,
        .mutex=PTHREAD_MUTEX_INITIALIZER

};

int is_empty( ){
        return queue.nitem == 0;
}

int is_full( ){
        return queue.nitem ==  Q_MAX-1;
}

void put( int d ){

        queue.data[queue.head] = d;
        queue.head = (queue.head+1)%Q_MAX;
        queue.nitem++;
}

int get(){

        int temp =  queue.data[queue.tail];
        queue.tail = (queue.tail+1)%Q_MAX;
        queue.nitem--;
        return temp;
}

// ==============queue implementation end ====================

void * produce(void *arg){

        int i=0;
        while(1){

                pthread_mutex_lock(&queue.mutex);

                if(is_full()){
                        pthread_cond_wait(&queue.c_cond,&queue.mutex);
                }

                printf("produce:%d\n",i);

                put(i);
                pthread_cond_signal(&queue.p_cond);

                pthread_mutex_unlock(&queue.mutex);
                i++;

                if(i==100) break;

                usleep(rand()%1000);
        }
        return 0;

}



void * consume(void *arg){
        while(1){

                pthread_mutex_lock(&queue.mutex);

                if(is_empty()){
                        pthread_cond_wait(&queue.p_cond,&queue.mutex);
                }
                int item=get();

                pthread_cond_signal(&queue.c_cond);
                printf("\t\tconsume:%d\n",item);

                pthread_mutex_unlock(&queue.mutex);

                usleep(rand()%1000);
        }

        return 0;

}



int main(int argc, char **argv){

        int     n;
        pthread_t producer, consumer;
        srand(time(0));

        pthread_create(&producer, 0, &produce, 0);
        pthread_create(&consumer, 0, &consume, 0);

        pthread_join(producer, 0);
        pthread_join(consumer, 0);

        return 0;

}

 

자 이제 컴파일 후 결과를 봅시다.

# gcc producer_consumer.c -lpthread
# ./a.out
produce:0
                consume:0
produce:1
                consume:1


...


produce:93
produce:94
produce:95
                consume:93
produce:96
                consume:94
produce:97
                consume:95
produce:98
                consume:96
produce:99
                consume:97
                consume:98
                consume:99

 

이상으로 간단한 조건변수 사용법을 알아보았습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

동기화와 동기화를 이용한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

임계 영역(Critical Section) 

mutex를 알아보기전에 우선 critical section(임계구역)부터 간단하게 알아보자면 critical section은 하나의 한 스레드만이 진입해야하는 특정 코드 구역을 말합니다. 다시 말해 공유자원의 변경이 일어날 수 있는 구간이 임계 영역입니다. 공유자원이라고 하면 여러가지가 있을 수 있는데 간단히 변수라고 생각하세요.

 

예를 들어볼까요? 자, 아래코드의 임계영역은 cnt=0으로 초기화하며 for루프를 실행하는 구역입니다. 여기에 공유자원은 cnt가 되지요. 스레드가 2개가 있고 차례대로 create하게 됩니다. 아래의 소스코드가 각각 스레드가 실행부가 됩니다. 이 코드의 실행 결과를 한번 예측해보세요. 

void *count(void *arg){
    int i;
    char* name = (char*)arg;

    //======== critical section =============
    cnt=0;
    for (i = 0; i <10; i++)
    {
        printf("%s cnt: %d\n", name,cnt);
        cnt++;
        usleep(1);
    }
    //========= critical section ============
}

 

우리의 예측은 이렇습니다. 

thread1이 count함수 실행 : cnt를 0으로 초기화하고 cnt를 10번 증가시킨 후 종료

thread2가 count함수 실행 : cnt를 0으로 초기화하고 cnt를 10번 증가시킨 후 종료

 

하지만 실제 결과는 다르지요. 아래와 같이 뒤죽박죽으로 나옵니다.

thread2 cnt: 0
thread1 cnt: 0
thread1 cnt: 1
thread1 cnt: 2
thread2 cnt: 3
thread1 cnt: 4
thread2 cnt: 5
thread1 cnt: 6
thread1 cnt: 7
thread2 cnt: 8
thread2 cnt: 9
thread1 cnt: 10
thread2 cnt: 11
thread1 cnt: 12
thread1 cnt: 13
thread2 cnt: 14
thread1 cnt: 15
thread2 cnt: 16
thread2 cnt: 17
thread2 cnt: 18

 

뮤텍스(MutEx)

Mutual Exclusion의 약자로 상호배제라고 합니다. 특정 쓰레드 단독으로 들어가야되는 코드 구역에서 동기화를 위해 사용되는 동기화 기법입니다.

우리는 리눅스에서 이 뮤텍스를 통한 동기화를 수행하여 위 코드의 문제점을 해결해볼겁니다. 

우선 원래의 문제가 되는 모든 코드는 아래와 같습니다.

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int cnt=0;

void *count(void *arg){
    int i;
    char* name = (char*)arg;
    
    //======== critical section =============
    cnt=0;
    for (i = 0; i <10; i++)
    {
        printf("%s cnt: %d\n", name,cnt);
        cnt++;
        usleep(1);
    }
    //========= critical section ============
}

int main()
{
    pthread_t thread1,thread2;

    pthread_create(&thread1, NULL, count, (void *)"thread1");
    pthread_create(&thread2, NULL, count, (void *)"thread2");

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
}

 

문제해결을 위해서 우리가 생각해볼 수 있는 것은 cnt = 0 전에 먼저 실행되는 스레드가 어떤 잠금장치를 이용해 잠그고, 나올때 잠금을 해제하면 되겠다는 생각을 해볼 수 있겠네요. 이런 목적을 달성하기 위해 우리는 4개의 pthread mutex함수를 기억하면 됩니다. 이 함수들은 pthread.h내에 존재합니다.

 

pthread_mutex_init : mutex를 초기화하는데에는 두 가지 방법이 존재합니다.

 

1) 정적으로 할당된 뮤텍스를 초기화하려면 PTHREAD_MUTEX_INITIALIZER 상수를 이용해서 초기화합니다.

이런 형식으로 사용합니다. : pthread_mutex_t lock = PTHREAD_MUTX_INITIALIZER;

2) 동적으로 초기화하려면 pthread_mutex_init 함수를 사용하면 됩니다. mutex를 사용하기 전에 초기화를 시작해야합니다. 

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

 

첫번째 인자는 mutex, 두번째인자는 이 mutex의 속정을 주는데, 기본적으로 NULL을 사용합니다.

 

pthread_mutex_lock, pthread_mutex_unlock : 이 두 함수는 mutex를 이용하여 임계 구역을 진입할때 그 코드 구역을 잠그고 다시 임계 구역이 끝날때 다시 풀어 다음 스레드가 진입할 수 있도록 합니다.

 

한 가지 중요한 점은 pthread_mutex_lock이 어떤 스레드에서 호출되어 lock이 걸렸을때 다른 스레드가 임계구역에 진입하기 위해서 pthread_mutex_lock을 호출했다면 그 스레드는 이 전의 스레드가 임계 구역을 나올때까지, 즉, pthread_mutex_unlock을 할때까지 기다려야합니다. 

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

pthread_mutex_destroy : 만약 뮤텍스를 동적으로 생성(pthread_mutex_init을 이용하여 초기화)했다면 이 함수를 사용하는 함수가 pthread_mutex_destroy입니다.

int pthread_mutex_destroy(pthread_mutex_t *mutex);

 

이제 문제를 해결하는 코드를 봐야겠네요.

문제를 해결한 코드는 아래와 같습니다. 

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

pthread_mutex_t mutex;
int cnt=0;

void *count(void *arg){
    int i;
    char* name = (char*)arg;

    pthread_mutex_lock(&mutex);

    //======== critical section =============
    cnt=0;
    for (i = 0; i <10; i++)
    {
        printf("%s cnt: %d\n", name,cnt);
        cnt++;
        usleep(1);
    }
    //========= critical section ============
    pthread_mutex_unlock(&mutex);
}

int main()
{
    pthread_t thread1,thread2;

    pthread_mutex_init(&mutex,NULL);

    pthread_create(&thread1, NULL, count, (void *)"thread1");
    pthread_create(&thread2, NULL, count, (void *)"thread2");

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_mutex_destroy(&mutex);
}

 

critical section 전, 후에 lock, unlock을 하는 것과 프로그램 시작 직후, 종료 직전에 mutex를 초기화하고 제거하는 과정만 추가되었습니다. 

이제 컴파일하고 실행결과를 보도록 합시다.

#gcc pthread_mutex.c -lpthread
# ./a.out
thread2 cnt: 0
thread2 cnt: 1
thread2 cnt: 2
thread2 cnt: 3
thread2 cnt: 4
thread2 cnt: 5
thread2 cnt: 6
thread2 cnt: 7
thread2 cnt: 8
thread2 cnt: 9
thread1 cnt: 0
thread1 cnt: 1
thread1 cnt: 2
thread1 cnt: 3
thread1 cnt: 4
thread1 cnt: 5
thread1 cnt: 6
thread1 cnt: 7
thread1 cnt: 8
thread1 cnt: 9

 

차례대로 들어간 스레드부터 0~9까지 출력하는 것을 볼 수 있습니다. 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

메시지큐, 공유메모리 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

메시지 큐

IPC기법 중 하나인 메시지큐는 Data Structure 중 큐를 사용합니다. 기본적으로는 먼저온 메시지가 먼저 꺼내어집니다. 메시지큐의 msgtype에 따라 특정 메시지 중 가장 먼저들어온 메시지도 받아올 수 있습니다. 이 메시지는 커널에서 보관하고 있으니 프로세스가 종료되어도 사라지지 않습니다.

메시지 큐의 용량이 허용하는 한 메시지는 계속 큐에 쌓일 수 있습니다. 메시지를 얻어오는 쪽은 가장 메시지를 읽고 메시지큐에서 그 메시지를 삭제합니다.

 

메시지 큐에는 두가지의 종류가 있습니다.

- System V의 Message Queue

- POSIX의 Message Queue

mq_open, mq_send, mq_receive와 같은 함수를 사용하는 POSIX에서 메시지큐 활용방법을 알아보시려면 아래의 포스팅을 참고하시면 됩니다.

https://reakwon.tistory.com/209

 

[리눅스] POSIX 메시지 큐 사용 방법 - 예제 코드

Message Queue 프로세스 간 통신 방식인 IPC기법 중 하나인 메시지 큐는 두 가지 사용 방법이 있습니다. msgget, msgsend, msgrecv와 같은 함수를 사용하는 방식인 System V 방식의 메시지큐, 그리고 지금 알아

reakwon.tistory.com

이번 포스팅에서는 System V의 Message Queue를 리눅스에서 어떻게 이용할 수 있는지 알아보도록 합시다.

 

메시지 큐 관련 시스템 콜

메시지큐를 사용하기 위해서는 3개의 헤더파일이 필요합니다.

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

 

1) msgget : System V의 메시지 큐 id를 얻어옵니다.

 int msgget(key_t key, int msgflg);

 

key : 메시지큐를 얻어올 때 사용하는 고유 key값입니다.

msgflg : flag에는 2가지가 있는데요. IPC_CREAT과 IPC_EXCL입니다.

 - IPC_CREAT : 메시지큐가 없으면 새로 생성합니다.

 - IPC_EXCL : IPC_CREAT과 같이 사용하는 flag인데, 만약 해당 메시지큐가 존재하면 msgget은 오류를 반환합니다.

 

2) msgsnd : 메시지를 보냅니다. 

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

 

msqid : 메시지 큐의 id입니다.

msgp : msgp는 void*이나 우리는 구조체 형식으로 아래와 같이 정의해주어야합니다. 

struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[1];    /* message data */
};

 

- mtype : 메시지의 타입을 나타냅니다. 이 값은 0보다 커야한다고 주석이 말해주고 있군요. 메시지큐의 msgbuf를 정의할때 long형의 mtype을 반드시 명시해주어야합니다. 

- mtext : mtext은 실제 메시지 큐에 보낼 데이터를 말합니다. mtext와 같이 배열일 수도 있고, 구조체일 수도 있습니다. 

msgsz : msgsz는 메시지 큐에 전송할 데이터의 사이즈를 의미하는데, 위의 msgbuf의 mtype멤버를 제외한 실데이터의 크기를 전달해야합니다. 

msgflg : 큐의 공간이 없을때 msgsnd의 동작은 blocking입니다. 즉, msgsnd에서 큐의 공간이 생겨날때까지 기다리는 것이지요. 여기서 IPC_NOWAIT을 사용한다면 msgsnd는 blocking되지 않고 실패합니다.

msgsnd가 성공적으로 동작하면 msqid_ds라는 구조체의 필드의 값이 변경됩니다. sys/msg.h에 이 구조체가 정의되어있습니다.

 

struct msqid_ds {
       struct ipc_perm msg_perm;     /* Ownership and permissions */
       time_t          msg_stime;    /* Time of last msgsnd(2) */
       time_t          msg_rtime;    /* Time of last msgrcv(2) */
       time_t          msg_ctime;    /* Time of last change */
       unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
       msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
       msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
       pid_t           msg_lspid;    /* PID of last msgsnd(2) */
       pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

이 구조체는 메시지큐의 메타데이터같은 것인데 msgsnd를 성공적으로 호출하고 나면 아래와 같이 변경됩니다.

- msg_lspid는 호출된 process id로 변경

- msg_qnum의 값 1 증가

- msg_stime을 현재 시간으로 설정

 

3) msgrcv : msgsnd를 했다면 받는 시스템 콜이 있어야겠지요. msgrcv가 그 역할을 합니다. 메시지큐 id의 메시지를 하나 읽고 그 메시지를 큐에서 제거합니다.

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

 

msqid : 다 아시겠지만 메시지큐 id입니다.

msgp : 읽어들인 메시지를 msgp가 가리키는 주소에 위치시킵니다. 쉽게말해 읽어온 메시지입니다.

msgsz : 메시지를 읽을 크기(더 정확히는 msgbuf의 text 크기입니다.)인데 만약 읽어들인 메시지가 지정된 크기보다 크다면 msgflg에 따라 동작이 결정됩니다. 만약 msgflg가 MSG_NOERROR라면 메시지를 읽어들이나 잘려서 읽히게 됩니다. MSG_NOERROR가 명시되어있지 않다면 메시지를 읽어오지 않고 msgrcv 시스템콜은 -1을 반환합니다.

msgtyp : 0, >0, <0으로 동작이 나뉩니다.

msgtyp == 0 큐의 첫번째 메시지를 읽어옵니다.
msgtyp > 0 그 값과 동일한 메시지 타입을 갖는 메시지를 반환합니다. 
msgtyp < 0 msgtyp의 절대값 이하의 가장 작은 메시지를 읽어옵니다.

msgflg : 4개 정도의 flag가 있습니다. 

- IPC_NOWAIT

- MSG_COPY : 리눅스 3.8이상부터 지원합니다.

- MSG_EXCEPT

- MSG_NOERROR

 

4) msgctl : 메시지큐를 제어하기 위한 시스템 콜입니다. 

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msqid : 다들 아시죠? 메시지 큐 id입니다.

cmd : 제어할 command입니다. 몇개 있는데 3개만 보도록 하겠습니다.

- IPC_STAT : buf로 메시지큐 정보를 읽어옵니다. 

- IPC_SET : buf로 메시지큐 정보를 설정합니다.

- IPC_RMID : 메시지큐를 지웁니다.

buf : 아까 위에서 보았던 구조체네요. cmd로 IPC_STAT과 IPC_SET을 사용할때 전달해주면 되고 

딱히 필요없으면 NULL을 전달합니다.

 

이제 메시지큐를 통한 예제를 볼까요?

아래의 예제는 sender에서 어떤 사람의 나이와 이름을 receiver에게 전달하는 메시지 큐 예제입니다. sender에서는 메시지큐의 정보 또한 보여주고 있습니다. 메시지 큐에 있는 message라는 구조체는 sender와 receiver가 사용하고 있으므로 헤더파일 msg_data.h를 두어 같이 사용합니다.

 

예제는 너무 간단하기 때문에 따로 설명할 필요는 없고 한번 따라 해보면 다 이해하실거에요.

 

msg_data.h

struct real_data{
        short age;
        char name[16];
};
struct message{
        long msg_type;
        struct real_data data;
};

 

sender.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "msg_data.h"


void printMsgInfo(int msqid){

        struct msqid_ds m_stat;
        printf("========== messege queue info =============\n");
        if(msgctl(msqid,IPC_STAT,&m_stat)==-1){
                printf("msgctl failed");
                exit(0);
        }
        printf(" message queue info \n");
        printf(" msg_lspid : %d\n",m_stat.msg_lspid);
        printf(" msg_qnum : %d\n",m_stat.msg_qnum);
        printf(" msg_stime : %d\n",m_stat.msg_stime);

        printf("========== messege queue info end =============\n");
}
int main(){
        key_t key=12345;
        int msqid;

        struct message msg;
        msg.msg_type=1;
        msg.data.age=80;
        strcpy(msg.data.name,"REAKWON");

        //msqid를 얻어옴.
        if((msqid=msgget(key,IPC_CREAT|0666))==-1){
                printf("msgget failed\n");
                exit(0);
        }

        //메시지 보내기 전 msqid_ds를 한번 보자.
        printMsgInfo(msqid);

        //메시지를 보낸다.
        if(msgsnd(msqid,&msg,sizeof(struct real_data),0)==-1){
                printf("msgsnd failed\n");
                exit(0);
        }

        printf("message sent\n");
        //메시지 보낸 후  msqid_ds를 한번 보자.
        printMsgInfo(msqid);
}

 

receiver.c 

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "msg_data.h"

int main(){
        key_t key=12345;
        int msqid;
        struct message msg;

        //받아오는 쪽의 msqid얻어오고
        if((msqid=msgget(key,IPC_CREAT|0666))==-1){
                printf("msgget failed\n");
                exit(0);
        }
        //메시지를 받는다.
        if(msgrcv(msqid,&msg,sizeof(struct real_data),0,0)==-1){
                printf("msgrcv failed\n");
                exit(0);
        }

        printf("name : %s, age :%d\n",msg.data.name,msg.data.age);

        //이후 메시지 큐를 지운다.
        if(msgctl(msqid,IPC_RMID,NULL)==-1){
                printf("msgctl failed\n");
                exit(0);
        }
}

 

두개의 c파일을 컴파일 해줍시다.

# gcc sender.c -o sender
# gcc receiver.c -o receiver

 

이제 sender부터 실행하도록 합시다.

# ./sender
========== messege queue info =============
 message queue info
 msg_lspid : 0
 msg_qnum : 0
 msg_stime : 0
========== messege queue info end =============
message sent
========== messege queue info =============
 message queue info
 msg_lspid : 5129
 msg_qnum : 1
 msg_stime : 1586521322
========== messege queue info end =============

 

메시지 큐의 정보가 변경된 것을 알 수있네요. 메시지가 성공적으로 보내졌나봅니다.

ipcs를 구경해볼까요?

# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x00003039 5          root       666        18

 

0x3039는 우리가 정한 key 12345입니다. sender 프로그램이 끝나고 메시지 큐는 아직 남아있습니다. 

이제 receiver를 실행해보도록 하지요.

# ./receiver
name : REAKWON, age :80

 

음, 잘 읽히네요. 한번더 receiver를 실행하면 아무런 동작을 하지 않습니다. 메시지큐에 아무것도 없기 때문입니다. ipcs를 들여다보면 메시지큐가 삭제되었네요. 그 이유는 receiver 코드를 보면 msgctl에서 cmd를 IPC_RMID를 주어 제거했기 때문입니다. 

# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

 

이상으로 메시지큐와 관련한 포스팅을 마치도록 하겠습니다. 긴글 읽어주셔서 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

공유 메모리, 메시지큐 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

공유메모리(Shared Memory)

프로세스에서 메모리는 해당 프로세스만이 사용하는게 일반적입니다. 메모리에는 명령어, 지역 변수, 동적 변수, 전역 변수와 같이 데이터가 존재하는데 그 프로세스만 접근할 수 있고 변경가능합니다. 일반적으로 이렇다는 건데, 아주 가끔 이 데이터가 다른 프로세스에서 쓰일 수 있도록 만들 수도 있습니다. 그게 바로 공유메모리라는 IPC기법입니다.

서로 다른 프로세스가 특정 메모리를 공유하면 데이터를 더 빠르게 접근할 수 있기 때문에 프로그램을 더 효율적으로 만들 수 있습니다. 이해도 직관적으로 할 수 있기 때문에 코드도 그렇게 어렵지가 않습니다.

 

관련 함수

공유메모리를 사용하기 위해서 우리가 필요한 헤더파일은 아래와 같이 2개입니다.

 #include <sys/shm.h> 
 #include <sys/ipc.h>

 

1) shmget : 인자로 전달된 key의 값으로 공유메모리를 얻고 공유메모리 조각(shared memory segment)의 id를 돌려줍니다. 

int shmget(key_t key, size_t size, int shmflg);

key : 공유메모리를 할당할때 사용하는 고유 key값입니다.

size : 메모리의 최소 size를 의미합니다. 새로운 공유메모리를 할당받는다면 size를 명시하고 이미 존재하는 메모리면 0을 주면 됩니다.

shmflg : IPC_CREATIPC_EXCL 두 가지가 존재합니다.

- IPC_CREAT : 새로운 메모리 세그먼트는 만듭니다. 이 flag를 사용하지 않는다면 shmget은 명시된 key와 연관된 찾고 접근할 수 있는 권한이 있는지 확인합니다.

 

- IPC_EXCL : IPC_CREAT과 함께쓰는 플래그로 만약 메모리 세그먼트가 존재하면 shmget은 실패하게 됩니다.

IPC_EXCL을 사용하는 경우는 우선 공유메모리가 있는지 확인 후 없으면 IPC_CREAT을 통해 할당받으라는 뜻입니다. 이렇게 공유메모리가 오염되는 것을 방지할 수 있습니다.

 

2) shmat : 공유메모리를 얻었으면 메모리의 위치에 이 프로세스를 묶는(attach) 시스템 콜입니다.

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid : 공유메모리의 id를 의미합니다. shmget을 통해 얻어올 수 있습니다.

shmaddr

-NULL(0)일 경우 : 커널에서 적절한 주소를 반환하여 줍니다.

-NULL이 아닐 경우 : shmflg로 SHM_RND일때, 그 주소와 attach할 가장 가까운 주소를 반환합니다.

 

정상적으로 동작한다면 적절한 포인터를 넘기고 실패하면 (void*) -1을 반환하게 됩니다. 여기서 왜 void*를 반환하는지 아시겠죠? 공유 메모리에 들어가있는 데이터가 정수형인지, 부동 소수 형태인지, 또는 구조체인지 모르니까 무엇이든 받을 수 있는 void*으로 넘겨줄테니 알아서 반환해서 써라 이 이야기입니다.

 

3) shmdt : 공유메모리를 이 프로세스와 떼어냅니다. 이는 공유메모리를 제거하는 것이 아님에 주의하세요.

int shmdt(const void *shmaddr);

shmaddr : shmat에서 전달받은 그 포인터를 전달하면 됩니다.

성공 시 0, 실패시 -1을 반환합니다.

 

4) shmctl : 공유메모리를 제어하기 위해 사용합니다. 예를 들면 공유메모리의 정보를 얻거나 어떤 값을 쓰거나 공유메모리를 삭제하는 등의 조작이 있습니다.

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid : 말 안해도 아시겠지만 공유메모리 id입니다.

cmd : 제어할 일종의 command입니다. 정수형을 갖으며 여러 command가 존재하는데 우리는 IPC_RMID라는 cmd를 예제를 통해 사용하도록 하겠습니다.

buf : shmid_ds라는 구조체로 정의되어 있네요. 어떤 구조체인지 봅시다.

struct shmid_ds {
       struct  ipc_perm shm_perm;    /* Ownership and permissions */
       size_t          shm_segsz;   /* Size of segment (bytes) */
       time_t          shm_atime;   /* Last attach time */
       time_t          shm_dtime;   /* Last detach time */
       time_t          shm_ctime;   /* Last change time */
       pid_t           shm_cpid;    /* PID of creator */
       pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
       shmatt_t        shm_nattch;  /* No. of current attaches */
       ...
};

 

복잡하게 보이지만 사실 옆의 주석만 좀 읽어보면 추측은 할 수 있겠네요. 

shm_perm : 공유메모리의 접근제어가 저장되어있습니다.

shm_segsz : 공유메모리의 size를 의미합니다.

shm_atime : attach된 시간 정보를 기록하네요.

shm_dtime : 반대로 detach된 시간을 기록합니다.

shm_ctime : 공유메모리가 변경될때의 시간을 기록합니다.

shm_cpid : 이 공유메모리를 최초로 만들어낸 process id입니다.

shm_lpid : 최근 shmat, 또는 shmdt를 수행한 process id입니다. 

 

첫번째 필드인 ipc_perm이라는 구조체도 한번 볼까요?

struct ipc_perm {
       key_t          __key;    /* Key supplied to shmget(2) */
       uid_t          uid;      /* Effective UID of owner */
       gid_t          gid;      /* Effective GID of owner */
       uid_t          cuid;     /* Effective UID of creator */
       gid_t          cgid;     /* Effective GID of creator */
       unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
       unsigned short __seq;    /* Sequence number */
};

 

아실테지만 uid, gid 등의 permission 정보가 있습니다. 

 

 

 

 

예제 

이제 공유메모리를 사용한 아주 간단한 예를 보도록 하겠습니다. 너무나 간단합니다.

#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
        int shmid;
        int *num;
        key_t key=987654;
        void *memory_segment=NULL;

        if((shmid=shmget(key,sizeof(int),IPC_CREAT|0666))==-1){
                printf("shmget failed\n");
                exit(0);
        }


        if((memory_segment=shmat(shmid,NULL,0))==(void*)-1){
                printf("shmat failed\n");
                exit(0);
        }

        num=(int*)memory_segment;
        (*num)++;
        printf("shared memory value :%d\n",(*num));
        return 0;
}

 

공유메모리 id를 얻어와서 공유메모리의 값을 하나씩 증가시키고 있는 예제입니다. 보시다시피 공유메모리의 포인터는 void*를 반환하지만 우리는 int*로 연산할 것이기에 int*형변환을 하고 값을 증가시켰지요.

사용되는 key값은 아무 정수나 넣어주시면 됩니다. 

 

자, 컴파일하고 a.out을 연타해봅시다.

# gcc shm.c
# ./a.out
shared memory value :1
# ./a.out
shared memory value :2
# ./a.out
shared memory value :3
# ./a.out
shared memory value :4
# ./a.out

 

본래 프로세스가 끝나면 메모리도 정리가 되서 계속 1이라는 값이 출력이 되어야하는데 메모리가 아직 살아있고 그 메모리의 값을 참조하여 증가시키니 프로그램을 계속 실행시키면 이렇게 증가된 값을 보여주게 됩니다.

공유메모리가 아직 살아있다는 것을 어떻게 알까요?

 

ipcs -m 명령으로 볼 수 있습니다.

# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 4          root       600        524288     2          dest
0x000181cd 5          root       660        4          0
0x000181cb 6          root       666        4          0
0x000f1206 14         root       666        4          0

 

key값을 잘 보시기 바랍니다. 0x000f1206은 10진수로 987654, 즉 우리가 위 코드에 설정한 key값입니다.

 

저희는 프로그램이 종료되면 공유메모리도 종료시키고 싶습니다. 가령 아주 위험하게 이 공유메모리에 민감한 정보라도 있다면 큰일이잖아요? www.poxxhub.com/post_id=xxxxx

 

 

 

다음과 같이 코드를 고쳐봅시다. 

#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
        int shmid;
        int *num;
        key_t key=987654;
        void *memory_segment=NULL;

        if((shmid=shmget(key,sizeof(int),IPC_CREAT|0666))==-1){
                printf("shmget failed\n");
                exit(0);
        }


        if((memory_segment=shmat(shmid,NULL,0))==(void*)-1){
                printf("shmat failed\n");
                exit(0);
        }

        num=(int*)memory_segment;
        (*num)++;
        printf("shared memory value :%d\n",(*num));
        if(shmctl(shmid,IPC_RMID,NULL)==-1){
                printf("shmctl failed\n");
        }

        return 0;
}

 

위 코드에 추가한것은 shmctl을 추가한것 밖에 없습니다. cmd로 IPC_RMID를 전달했네요. 그렇다면 이제 이 shmid는 사용할 수 없게 됩니다.

이제 컴파일하고 실행해봅시다.

# gcc shm.c
# ./a.out
shared memory value :5
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1

 

다시 ipcs -m 명령으로 공유메모리가 남아있는지 확인해보면 사라진것을 볼 수 있습니다.

# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 4          root       600        524288     2          dest
0x000181cd 5          root       660        4          0
0x000181cb 6          root       666        4          0

 

여기까지 공유메모리에 대해서 살펴보았는데요. IPC기법중에서도 그렇게 어렵지 않는 공유메모리를 사용할때 주의해야할 한 가지는 데이터가 오염되지 않게 처리하는 방법입니다. semaphore이든 mutex이든 메모리를 동시에 접근할때 처리를 해주어야하며 이 포스팅에서는 그러한 처리는 하지 않았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

세마포어와 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

세마포어(Semaphore)란

세마포어(Semaphore)는 사전적 의미는 수기 신호라고 합니다. 다익스트라라는 학자가 고안해낸 이 기법은 두 개 이상의 프로세스가 동시에 공유 메모리와 같은 공유 자원을 접근할때 동기화를 걸어주는 것을 목표로 합니다.

 

다음과 같은 상황을 보도록 합시다.

 

프로세스 A, 프로세스 B는 int a=10이라는 자원을 공유합니다. 사용자는 두 프로세스에서 a를 1씩 증가시키는 작업을 시켰고 사용자는 a가 12라는 값이 될 것을 예상하고 있습니다.

a=10
1) 프로세스 A는 a의 값을 읽어온다. a=10
2) 프로세스 B는 a의 값을 읽어온다. a=10
3) 프로세스 A는 a의 값을 증가시킨다. a=11
4) 프로세스 B는 a의 값을 증가시킨다. a=11
a=11 

이렇게 사용자가 예상했던 것과는 다른 예기치못한 결과를 초래합니다. 이런 결과가 된 이유는 무엇일까요?

우선 a는 두 프로세스에서 접근이 가능한 공유자원입니다. 이것을 동시에 임계영역(Critical section)에 진입하여 공유자원에 접근할때 문제가 발생한게 그 원인이지요. 이처럼 한번에 여러 프로세스가 접근하여 데이터를 동시에 변경하는 것을 막기 위한 장치가 바로 세마포어라고합니다.

임계 영역(critical section)
공유자원에 접근할 수 있는 영역을 말하게 됩니다. 이런 임계 영역은 반드시 보호되어야하는 구간으로 보호 메카니즘으로는 세마포어와 뮤텍스 등이 있습니다. 

세마포어는 P연산, V연산으로 이루어져있으며 지금부터 P,V연산이 무엇인지 아주 간단하게 정의하면 아래와 같습니다.

P : S를 1 감소
V : S를 1 증가

 

이 연산을 이용해 임계영역에 어떻게 동기적으로 진입할 수 있을까요?

우선 S를 1로 생각해보고 프로세스는 S가 1일때만 임계영역으로 진입할 수 있다고 보겠습니다. 그렇다면 S가 0이면 진입하지 못하겠군요. 그렇다면 아래와 같이 임계영역에 접근할 수 있겠네요.

초기 S=1
P(S) (S가 1감소되어 0)
//critical section start
// ...공유 자원을 사용할 수 있는 영역
//critical section end
V(S) (S가 1증가되어 1)

P(S)를 수행하면 S가 1이 감소되어 0이지요. 만약 다른 프로세스가 이 임계영역을 보고 S가 0이면 대기합니다. 그러니까 P연산에는 S가 0이라면 대기하는 코드가 들어가겠네요. 만일 0이 아니라면, 즉 1보다 크다면 S를 1감소시킨 후에 임계영역으로 들어갈 수 있게됩니다.P(S)는 어떻게 구현되어있을까요?

P(S){
    while(S==0){
         //wait
    }
    S--;
}

 

그렇다면 V(S)는 어떻게 구현되어있을까요?

V(S)는 S를 하나 증가시킨다고 했지요? 그렇게함으로써 P(S)에서 기다리고 있는 프로세스가 S가 1이 되는 순간 진입할 수 있게 해주거든요. 대략  아래와 같은 코드로 구현되어있겠네요.

V(S){
    S++;
}

** 오해하지 마세요! 세마포어는 실제 완전히 저렇게 생기지 않습니다. 단순한 이해를 위해서 저런 가상의 코드를 구현해놓은 것입니다. 

 

우리는 여기서 S에 주목할 필요가 있습니다. 만일 S가 1이라면 임계영역에 들어갈 수 있는 프로세스는 하나가 되죠. 그렇다면 S가 2이면 임계영역에 들어갈 수 있는 프로세스는 2개가 된다는 의미겠네요.

 

이렇게 Mutex와 Semephore가 차이가 생겨나게 됩니다. 

Mutex Semaphore
lock, unlock의 상태만 존재하는 일종의 binary semaphore 여러개의 프로세스가 동시에 공유자원에 접근할 수도 있음

 

 

Semapore 시스템콜

다음의 함수를 사용하기에 앞서 우리가 필요한 헤더파일은 다음과 같이 3개입니다.

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

 

1. semget : semid라는 세마포어 식별자를 얻는데 쓰이는 시스템콜입니다. 세마포어는 집합이라 같은 집합에 속하여 index로 구분됩니다. 보통은 한 집합에 1개의 세마포어를 사용합니다.

int semget(key_t key, int nsems, int semflg);

- key : 세마포어를 식별하는 키입니다.

- nsems :  세마포어 자원의 갯수를 의미합니다.

- semflg : 세마포어 동작옵션인데요. IPC_CREAT과 IPC_EXCL 두개가 존재합니다. 

IPC_CREAT: 새로운 세마포어를 만듭니다. 

IPC_EXCL : IPC_CREAT과 같이 사용하는데, 이미 세마포어가 존재할 경우 Error를 반환합니다.

 

호출 성공시 semid라는 세마포어 식별자를 반환합니다.

 

2. semctl : 세마포어를 제어할 수 있는 시스템 콜입니다.

int semctl(int semid, int semnum, int cmd, ...);

- semid: 세마포어의 식별자입니다. 이는 위의 semget으로부터 나온 id값입니다.

- semnum : semaphore 집합에서 표현되는 일종의 인덱스입니다.

- cmd : 세마포어를 제어할 수 있는 command인데요. 이것에 따라 semctl이 3개의 인자를 갖느냐, 4개의 인자를 갖느냐가 결정됩니다. 

- union semun : cmd에 의해 4번째 인자가 쓰일때 여러분이 작성하는 프로그램에서는 아래의 union을 정의해주어야합니다. 

 union semun {
        int  val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
  };

 

- struct semid_ds : semun에 멤버 semid_ds라는 구조체는 <sym/sem.h>에 아래와 같이 정의되어있습니다.

struct semid_ds {
       struct ipc_perm sem_perm;  /* Ownership and permissions */
       time_t sem_otime; /* Last semop time */
       time_t sem_ctime; /* Last change time */
       unsigned long   sem_nsems; /* No. of semaphores in set */
};

 

3. semop : 세마포어의 값을 증가, 감소시킴으로써 크리티컬 섹션 전 후에 사용됩니다.

int semop(int semid, struct sembuf *spos, size_t nsops);

 

- semid : 역시 semget에서 받은 semid를 사용합니다.

- spos : sembuf라는 구조체 포인터네요. sembuf는 어떻게 생겼을까요?

unsigned short sem_num;  /* semaphore number */
 short sem_op;   /* semaphore operation */
 short sem_flg;  /* operation flags */

 

3가지 필드로 구성되어있으며 sem_num은 세마포어 번호, sem_op는 증감값이며 이는 원자적으로 처리됩니다. sem_flg는 옵션입니다.

원자적연산 : 원자는 물체를 더 이상 쪼갤수 없는 단위입니다. 컴퓨터 연산의 원자적이라함은 더 이상 쪼개지지 않는 연산을 의미하여 한 사이클에 동작하는 연산을 의미합니다. a++이라는 연산은 메모리에 1) a의 값을 읽어들이고, 2)a의 값을 1개 증가시키고, 3) a를 다시 메모리에 저장하는 과정을 거치는데 이때 1), 2), 3) 각각이 원자적 연산이며 a++자체는 원자적 연산이 되지 않습니다. 만일 1), 2), 3)의 과정에서 어떠한 방해도 받지 않고 수행할 수 있다면 1)2)3)은 a++은 원자적 연산이라고 할 수 있습니다.

  

이제 시스템콜은 적당히 본것같고 예제를 보도록 합시다.

 

우선 세마포어가 적용되지 않은 쓰레드 2개를 돌리는 예제입니다.

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <stdlib.h>
#include <pthread.h>

int sharedVal=0;
int semid;

void s_wait(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=-1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void s_quit(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void *thread1_func(void *arg){
        int i;
        for(i=0;i<1000000;i++){
                //s_wait();
                sharedVal++;
                //s_quit();
        }
}


void *thread2_func(void *arg){

        int i;
        for(i=0;i<1000000;i++){
                //s_wait();
                sharedVal++;
                //s_quit();
        }
}

union semun{
        int val;
        struct semid_ds *buf;
        unsigned short *array;
};

int main(int argc, char *argv[]){
        pthread_t thread1;
        pthread_t thread2;
        union semun su;

        if((semid = semget(IPC_PRIVATE, 1, IPC_CREAT|0660)) == -1){
                printf("semet() fail \n");
                exit(0);
        }

        su.val=1;
        if(semctl(semid,0,SETVAL, su) == -1){
                printf("semctl fail\n");
                exit(0);
        }

        pthread_create(&thread1,NULL,thread1_func,NULL);
        pthread_create(&thread2,NULL,thread2_func,NULL);
        pthread_join(thread1,NULL);
        pthread_join(thread2,NULL);

        printf("shared val:%d\n",sharedVal);
        if(semctl(semid, 0, IPC_RMID, su) == -1){
                printf("semctl() fail\n");
                exit(0);
        }
        return 0;
}

 

 

이 코드를 컴파일하려면 gcc [파일명] -lpthread로 컴파일 하시기바랍니다.

저의 컴퓨터에서 실행한 결과 매번 다른 값을 보이고 있습니다. (머신마다 다르니 여러번 시도하거나 for loop을 더 많이 돌려보세요.) 우리가 예상하는 값은 가장 아래의 값입니다.

# ./a.out
shared val:1967612
# ./a.out
shared val:1109699
# ./a.out
shared val:2000000

 

반대로 주석을 풀어 세마포어를 적용시켜보도록 합시다.

저의 컴퓨터에서 실행한 결과 세마포어가 실행하는 시간이 있어 오버헤드가 증가하였으나 우리가 예상하는 값을 얻어낼 수가 있습니다.

# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000

 

예제를 조금 더 살펴보면 다음과 같습니다.

 if((semid = semget(IPC_PRIVATE, 1, IPC_CREAT|0660)) == -1){
        printf("semet() fail \n");
        exit(0);
 }

setget을 통해서 semid를 가져오는데, IPC_PRIVATE를 통해서 1개의 키를 생성하고 있네요. 

 

이후 semum을 통해서 semid를 갖는 세마포어를 제어합니다. 우선 semum의 val을 1로 셋팅합니다. 위의 P(S), V(S) 연산 기억나시나요? 이건 S의 초기값을 설멍하는 것과 같습니다.

이제 semid의 0번 인덱스의 값을 셋팅(SETVAL)하는데, su로 셋팅합니다. 

union semun{
        int val;
        struct semid_ds *buf;
        unsigned short *array;
};

int main(int argc, char *argv[]){
		//..
        union semun su;
		//..
       
        su.val=1;
        if(semctl(semid,0,SETVAL, su) == -1){
                printf("semctl fail\n");
                exit(0);
        }
        //..
 }

 

다음의 s_waits_quit은 임계영역에 들어가기전 기다리는 동작과 나오는 동작을 구현한 함수입니다. 

 

s_wait부터 봅시다. 자, sembuf에 있는 멤버를 들여다보면 semid라는 집합의 (buf.sem_num)0번 인덱스의 동작(buf.sem_op)을 -1로 규정하고 있습니다. 현재 세마포어의 값에 -1연산을 하라는 의미입니다. -2면 2를 빼는거겠죠.

SEM_UNDO는 프로그램이 종료될때 자동적으로 세마포어가 되돌려지는 옵션이라고 하네요. 잘 모르겠습니다 이건.

위의 P연산과 비슷한 역할을 하는 함수가 되겠죠.

 

s_quit은 반대로 sem_id의 0번 인덱스에 1을 더하라는 거겠네요. 그렇다면 위의 V연산과 비슷한 역할을 하는 함수가 됩니다.

void s_wait(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=-1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void s_quit(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

 

마지막으로 쓰레드가 모두 종료되고 프로그램이 종료되기 전에 semctl로 semid를 파기시킵니다. 

if(semctl(semid, 0, IPC_RMID, su) == -1){
         printf("semctl() fail\n");
         exit(0);
}

여기까지 세마포어의 개념과 간단한 예제를 보았습니다. 자세한것은 저도 잘 모르는 입장이라 공부하면서 더 보완을 해야겠네요.

 

읽어주셔서 감사합니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,