CA를 통한 인증서 검증과 Server - Client 통신

CA를 활용한 클라이언트 인증서 검증을 하기 위해서 CA의 인증서와 CA 서명된 Client인증서, Server 인증서가 필요합니다. 아래의 포스팅을 참고하면 CA, Server, Client의 키 쌍들과 CA 인증서와 Server, Client 인증서를 생성할 수 있습니다. 

https://reakwon.tistory.com/239

 

openssl CA를 통한 Server - Client 인증서 검증 및 대칭키 공유 과정

CA 인증서로 상대방의 인증서 확인 방법 디렉토리 구조 디렉토리 구조는 아래와 같으며 각각 인증서를 생성하는 과정에서 여러 키와 인증서가 생성이 될 겁니다. # ls CA Client Server CA : Root CA로 CA

reakwon.tistory.com

이렇게 하면 디렉토리의 구조는 아래와 같게 됩니다. 

# ls
Client  RootCA  Server

 

Server/server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#include <openssl/rsa.h>    
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
 
#define CHK_NULL(x) if((x) == NULL) exit(1);
#define CHK_ERR(err, s) if((err) == -1) { perror(s); exit(1); }
#define CHK_SSL(err) if((err) == -1) { ERR_print_errors_fp(stderr); exit(2); }
static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx){

    char *str;
    X509 *cert = X509_STORE_CTX_get_current_cert(ctx);

    if (cert) {
        printf("Cert depth %d\n", X509_STORE_CTX_get_error_depth(ctx));
                
        str = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
        CHK_NULL(str);
        printf("\t subject : %s\n", str);
        OPENSSL_free(str);

        str = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
        CHK_NULL(str);
        printf("\t issuer : %s\n", str);
        OPENSSL_free(str);
    }

    return preverify_ok;
}
 
int main(void){

    int err, listen_fd, socket_fd;
    struct sockaddr_in server, client;
    size_t client_len;
   
    // SSL 관련 객체 
    SSL_CTX *ctx;
    SSL *ssl;
    X509 *client_cert;
    SSL_METHOD  *meth;
    
    char *str;
    char buf[128];
   
    printf("Server start!\n");

    // SSL 초기 셋팅 
    SSL_load_error_strings();
    SSLeay_add_ssl_algorithms();
    meth = SSLv23_server_method();
    ctx = SSL_CTX_new(meth);  
   
    if(!ctx) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // 서버의 인증서 설정 
    if(SSL_CTX_use_certificate_file(ctx, "./Server.crt", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // 서버의 개인키 설정 
    if(SSL_CTX_use_PrivateKey_file(ctx, "./privkey-Server.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }

    // 개인키 사용 가능성 체크
    if(!SSL_CTX_check_private_key(ctx)) {
        fprintf(stderr, "private key is not matched to public key.\n");
        exit(-1);
    }
    // 사용할 CA의 인증서 설정
    if(!SSL_CTX_load_verify_locations(ctx, "../RootCA/CA.crt", NULL)) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }

    // Client의 인증서를 검증하기 위한 설정, CA는 하나만 있다고 가정-> depth = 1
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, verify_callback);
    SSL_CTX_set_verify_depth(ctx, 1); 
   
    // TCP socket 생성 이후 bind, listen, accept
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    CHK_ERR(listen_fd, "socket");
   
    memset(&server, 0x00, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(12340); 
 
    err = bind(listen_fd, (struct sockaddr*)&server, sizeof(server));
    CHK_ERR(err, "bind");
   
    err = listen(listen_fd, 5);
    CHK_ERR(err, "listen");
    
    client_len = sizeof(client);
    socket_fd = accept(listen_fd, (struct sockaddr*)&client, &client_len);
    CHK_ERR(socket_fd, "accept");
    close(listen_fd);
    
    // SSL 세션 생성 
    ssl = SSL_new(ctx);
    CHK_NULL(ssl);

    // SSL 접속 대기 , SSL_accept 완료 = SSL handshake 완료
    SSL_set_fd(ssl, socket_fd);
    err = SSL_accept(ssl);    
    CHK_SSL(err);
   
    // 사용하는 Cipher 
    printf("SSL is using cipher %s\n", SSL_get_cipher(ssl));
   
    // 클라이언트의 인증서를 받고 검증
    client_cert = SSL_get_peer_certificate(ssl);
    CHK_NULL(client_cert);


    if(SSL_get_verify_result(ssl) == X509_V_OK){
        printf("verify cert OK\n");
        X509_free(client_cert);
    } else {
        printf("verify cert Failed\n");
    }

   
    // 클라이언트로부터 메시지 수신
    err = SSL_read(ssl, buf, sizeof(buf)-1);
    CHK_SSL(err);
    buf[err] = 0;
    printf("From Client '%s'\n", buf);
   
    err = SSL_write(ssl, "Hello, Client!", strlen("Hello, Client!"));
    CHK_SSL(err);
   
    //자원 해제 
    close(socket_fd);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
   
    return(0);
}

  • SSL Context : Context는 cipher, TLS 버전, 인증서 및 암호 파라미터들의 모음입니다. 이를 기반으로 SSL session이 생성됩니다.  그러니까 인증서나 키 등의 세션들이 공통적으로 사용하는 데이터를 미리 설정해둠에 따라 세션이 만들어질때마다 이런 데이터를 미리 준비하는 과정이 없어집니다. 그 말은 즉, 시간이 줄어든다는 뜻입니다.
  • SSL Session : Session은 Server와 Client간의 연결이 실제 이루어진 것을 의미합니다. 이 세션에서 데이터의 전송이 이루어집니다. Server나 Client간의 세션이 없다면 만들어지고, 만들어져있다면 세션을 다시 재활용하는 것도 가능합니다. 

인증서 검증 

Client의 인증서를 검증하기 위해서는 SSL_CTX_set_verify, 그리고 SSL_CTX_set_verify_depth를 지정해야합니다. SSL_CTX_set_verify에 SSL_VERIFY_PEER 옵션 지정 후 verify_callback 함수를 통해서 커스텀한 검증을 할 수 있는데, verify_callback함수에 preverify_ok라는 인자는 이전에 검증 과정의 결과를 알려줍니다. depth에 따라서 여러번 호출이 됩니다.

위 verify_callback 함수는 SSL_CTX_set_verify_depth에 의해서 depth를 지정해 줄 수 있는데, depth에 따라 인증서를 어느 수준까지 인증할 것이냐를 정의해줄 수 있습니다. 예를 들어 depth가 1이면 Client(depth 0)와 그 인증서를 발행한 CA(depth 1)의 인증서를 검증하게 되는 것이구요. depth가 2일 경우에는 위 단계에서 CA의 인증서를 발생한 상위의 CA(depth 2)의 인증서를 검증합니다.

이후 잘 검증되었는지를 확인하는 SSL_get_verify_result 함수를 이용해 문제없이 인증서가 검증되었는지 확인할 수 있습니다.

 

Client/client.c

#include <stdio.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
 
#define CHK_NULL(x) if((x) == NULL) exit(1);
#define CHK_ERR(err, s) if((err) == -1) { perror(s); exit(1); }
#define CHK_SSL(err) if((err) == -1) { ERR_print_errors_fp(stderr); exit(2); }
 
int main(void)
{
    int err, socket_fd;
    struct sockaddr_in server;
   
    SSL_CTX *ctx;
    SSL *ssl;
    X509 *server_cert;
    char *str;
    char buf[128];
    SSL_METHOD    *method;
   
    //초기 세팅
    SSL_load_error_strings();
    SSLeay_add_ssl_algorithms();
    method = SSLv23_client_method();
    ctx = SSL_CTX_new(method);
    CHK_NULL(ctx);
   
    // Context에서 사용할 인증서 설정
    if(SSL_CTX_use_certificate_file(ctx, "./Client.crt", SSL_FILETYPE_PEM) <= 0) {    
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // Context에서 사용할 개인키 설정
    if(SSL_CTX_use_PrivateKey_file(ctx, "./privkey-Client.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(-1);
    }
   
    // 개인키 사용가능성 확인
    if(!SSL_CTX_check_private_key(ctx)) {
        fprintf(stderr, "Private key is not matched to public key\n");
        exit(-1);
    }
   
    // Socket의 Connect까지 설정하는 과정
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    CHK_ERR(socket_fd, "socket error ");
   
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    server.sin_port = htons(12340); 
   
    err = connect(socket_fd, (struct sockaddr*)&server, sizeof(server));
    CHK_ERR(err, "connect error ");
   
    //SSL 세션 객체 생성
    ssl = SSL_new(ctx); 
    CHK_NULL(ssl);
   
    //SSL객체에 socket fd를 설정
    SSL_set_fd(ssl, socket_fd);
    err = SSL_connect(ssl); 
    CHK_NULL(err);
   
    printf("SSL is using cipher %s\n", SSL_get_cipher(ssl));
   
    // 서버의 인증서를 가져옴
    server_cert = SSL_get_peer_certificate(ssl);
    CHK_NULL(server_cert);

    printf("Server certificate:\n");
   
    //인증서의 몇가지 정보를 출력
    str = X509_NAME_oneline(X509_get_subject_name(server_cert), 0, 0);
    CHK_NULL(str);
    printf("\t subject: %s\n", str);
    OPENSSL_free(str);
   
    /* 인증서의 issuer를 출력한다. */
    str = X509_NAME_oneline(X509_get_issuer_name(server_cert), 0, 0);
    CHK_NULL(str);
    printf("\t issuer: %s\n", str);
    OPENSSL_free(str);
   
    X509_free(server_cert);
   
    // 서버에게 메시지를 전송
    err = SSL_write(ssl, "Hello World!", strlen("Hello World!"));
    CHK_SSL(err);
   
    // 서버로부터 메시지 수신
    err = SSL_read(ssl, buf, sizeof(buf)-1);
    CHK_SSL(err);
    
    buf[err] = 0;
    printf("From Server : '%s'\n", buf);

   
    // 세션 종료 및 ssl, ctx 자원 해제
    SSL_shutdown(ssl);   
    close(socket_fd);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
   
    return 0;
}

 

결과화면

인증서를 대충만들었기 때문에 구분이 잘 안가실텐데, 아래의 depth 1은 CA 인증서, depth 0은 Client 인증서의 정보를 나타냅니다. CA의 인증서를 보면 subject와 issuer가 같은 것을 알 수 있죠. 자기 자신의 인증서를 자신이 사이닝했습니다.

Server
# ./server
Server start!
Cert depth 1
    subject : /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
    issuer : /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
Cert depth 0
    subject : /C=KR/ST=Some-State/O=Internet Widgits Pty Ltd/CN=Client/emailAddress=client@dd.com
    issuer : /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
SSL is using cipher TLS_AES_256_GCM_SHA384
verify cert OK
From Client 'Hello World!'
Client
# ./client
SSL is using TLS_AES_256_GCM_SHA384
Server certificate:
    subject: /C=KR/ST=Some-State/O=Internet Widgits Pty Ltd/CN=Server/emailAddress=server@dd.com
    issuer: /C=KR/ST=Some-State/O=CA/OU=CA/CN=CA/emailAddress=no
From Server : 'Hello, Client!'

 

참고한 자료

SSL Server - Client 코드 : http://pchero21.com/?p=603

CA, Server, Client 인증서 생성 : https://www.cs.toronto.edu/~arnold/427/19s/427_19S/tool/ssl/notes.pdf

인증서 검증 : https://tribal1012.tistory.com/m/213

 

반응형
블로그 이미지

REAKWON

와나진짜

,

CA 인증서로 상대방의 인증서 확인 방법

 

디렉토리 구조 

디렉토리 구조는 아래와 같으며 각각 인증서를 생성하는 과정에서 여러 키와 인증서가 생성이 될 겁니다.

# ls
CA  Client  Server

CA : Root CA로 CA의 개인키, 공개키, 인증서가 저장됩니다.

Client : Client의 개인키, 공개키, CSR, 인증서가 저장됩니다.

Server : Server의 개인키, 공개키, CSR, 인증서가 저장됩니다.

 

 

CA의 인증서 생성 절차

Root CA라고 가정하고 자신의 인증서를 생성하는 절차입니다.

1. private key 생성

# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:5 -out privkey-CA.pem

암호 알고리즘은 RSA를 사용하며 2048비트의 rsa 키 길이를 사용하는 개인키를 생성합니다. 추가로 rsa_keygen_pubexp로 exponent를 지정할 수 있습니다.

 

2. public key 생성

# openssl pkey -in privkey-CA.pem -pubout -out pubkey-CA.pem

생성된 private key의 쌍인 public key를 생성합니다.  

 

3.  Self-Sign한 인증서 생성

# openssl req -x509 -new -nodes -key privkey-CA.pem -sha256 -days 365 -out CA.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:State
Locality Name (eg, city) []:city
Organization Name (eg, company) [Internet Widgits Pty Ltd]:CA  
Organizational Unit Name (eg, section) []:CA
Common Name (e.g. server FQDN or YOUR name) []:CA
Email Address []:no

 

Server 인증서 생성 

서버의 인증서를 CA의 개인키로 서명하는 절차입니다. 개인키, 공개키를 생성하는 절차는 CA와 같습니다. 

1. private key 생성

# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:3 -out privkey-Server.pem

 

2. public key 생성

# openssl pkey -in privkey-Server.pem -pubout -out pubkey-Server.pem

 

3. CSR 생성

# openssl req -new -key privkey-Server.pem -out Server-req.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:Seoul
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Server
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:no

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

 

4. CA 서명된 인증서 Server 생성

# openssl x509 -req -in Server-req.csr -CA ../CA/CA.crt -CAkey ../CA/privkey-CA.pem -CAcreateserial -out Server.crt -days 500 -sha256
Certificate request self-signature ok
subject=C = KR, ST = Some-State, L = Seoul, O = Server, emailAddress = no

 

Client 인증서 생성

Client의 개인키, 공개키를 생성하고 CA의 개인키로 서명합니다.

1. private key 생성

# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:3 -out privkey-Client.pem

 

2. public key 생성

# openssl pkey -in privkey-Client.pem -pubout -out pubkey-Client.pem

 

3. CSR 생성

# openssl req -new -key privkey-Client.pem -out Client-req.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:Seoul
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Client
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:no

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

 

4.  CA 서명된 Client 인증서 생성

# openssl x509 -req -in Client-req.csr -CA ../CA/CA.crt -CAkey ../CA/privkey-CA.pem -CAcreateserial -out Client.crt -days 500 -sha256
Certificate request self-signature ok
subject=C = KR, ST = Some-State, L = Seoul, O = Client, emailAddress = no

 

인증서 검증

  • 클라이언트에서 서버 인증서 검증
# cd Client
# openssl verify -CAfile ../CA/CA.crt ../Server/Server.crt
../Server/Server.crt: OK

 

  • 서버에서 클라이언트 인증서 검증
# cd Server
# openssl verify -CAfile ../CA/CA.crt ../Client/Client.crt 
../Client/Client.crt: OK

 

공개키 추출

  • 서버 인증서에서 서버 공개키 추출
# cd Client
# cp ../Server/Server.crt .
# openssl x509 -pubkey -in Server.crt -noout > pubkey-Server.pem

 

  • 클라이언트 인증서에서 클라이언트 공개키 추출
# cd Server
# cp ../Client/Client.crt .
# openssl x509 -pubkey -in Client.crt -noout > pubkey-Client.pem

 

 

대칭키  공유

  • 클라이언트에서 랜덤한 대칭키 생성 후 Server의 공개키로 암호화 
# cd Client
# openssl rand -out symkey.pem -base64 32
# hexdump -C symkey.pem 
00000000  51 4f 31 67 6d 5a 63 5a  6e 36 76 47 48 31 36 37  |QO1gmZcZn6vGH167|
00000010  39 47 47 72 68 58 35 43  69 69 6f 4b 33 64 34 41  |9GGrhX5CiioK3d4A|
00000020  2b 6f 38 67 4a 4c 49 4a  64 65 38 3d 0a           |+o8gJLIJde8=.|
0000002d
# openssl pkeyutl -encrypt -in symkey.pem -pubin -inkey pubkey-Server.pem -out symkey.enc

 

  • 클라이언트의 개인키로 서명
# openssl dgst -sha1 -sign privkey-Client.pem -out signature.bin symkey.pem

 

  • 서버에서 암호화된 대칭키 복호화 후 검증

# cp ../Client/signature.bin .
# cp ../Server/symkey.enc .
# hexdump -C symkey.pem 
00000000  51 4f 31 67 6d 5a 63 5a  6e 36 76 47 48 31 36 37  |QO1gmZcZn6vGH167|
00000010  39 47 47 72 68 58 35 43  69 69 6f 4b 33 64 34 41  |9GGrhX5CiioK3d4A|
00000020  2b 6f 38 67 4a 4c 49 4a  64 65 38 3d 0a           |+o8gJLIJde8=.|
0000002d
# openssl dgst -sha1 -verify pubkey-Client.pem -signature signature.bin symkey.pem
Verified OK

 

이제 이 대칭키를 가지고 암복호화 통신을 하면 됩니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

dd 명령어 

dd 명령어는 파일을 복사하거나 sdcard 같은 저장장치에 데이터를 쓸 수 있게 해주거나, 혹은 반대로 저장장치나 파일로부터 데이터를 복사해올 수 있게 만들어주는 유용한 명령어입니다. 그래서 블록(Block)이라는 단위를 통해서 파일의 내용을 다른 파일이나 저장 장치로 쓸 수 있습니다. 이제부터 저장장치도 파일이라고 간주하고 설명하도록 하겠습니다.

사용법 1  :  파일 전체를 복사

기본적으로 dd 명령어는 아래와 같은 input file과 output file을 가지게 됩니다. 옵션이 아무것도 없죠. 이런 경우에는 파일의 전체를 복사합니다. 아래의 명령어는 /home/image.ext4 파일을 /dev/mmcblk0p2 파일로 복사합니다. 

# dd if=/home/image.ext4 of=/dev/mmcblk0p2
  • if : input file의 약자로 복사할 파일을 지정합니다.
  • of : output file의 약자로 출력이 될 파일을 지정합니다.

 

사용법2 : input의 내용을 output의 일부분으로 복사

위 명령어는 기존 output의 내용이 있던 없던 간에 무조건 input의 내용으로만 복사가 되었습니다. 하지만 output의 내용의 일부분만 input의 내용으로 복사할 경우나 추가할 경우에는 conv 옵션에 notrunc 값을 지정해야합니다. 참고로 여기서 conv는 convert의 줄임말입니다. 

# dd if=size512.bin of=size1024.bin conv=notrunc
  • conv=notrunc : output 파일의 내용을 자르지 않는다는 의미입니다.

 

사용법3 :  블록 사이즈 지정하고 원하는 바이트만큼 복사

블록사이즈는 사용자가 지정하기 나름인데 기본적으로 설정되어있는 블록 사이즈는 512바이트입니다. 블록 사이즈나 블록의 갯수를 지정하여 원하는 양의 크기대로 복사할 수 있습니다. bs 옵션은 block size의 의미로 블록의 크기를 지정하는데 사용됩니다. 만약 512바이트가 아닌 블록 사이즈가 1024바이트이길 원한다면 bs의 옵션에 1024를 지정해주면 됩니다. 그리고 그 블록을 몇개 복사할 것이냐를 지정할때는 count옵션을 사용하면 됩니다. 

아래와 같은 경우는 block size가 1이며 512의 블록을 output.bin으로 복사합니다. 이럴 경우에는 단순히 512바이트 만큼 복사가 되겠네요.

# dd if=/dev/vda1 of=output.bin bs=1 count=512
  • bs : block size의 의미로 블록의 크기
  • count : input file의 블록을 몇개 복사할 것인지를 지정

 

사용법4 : input 파일의 내용을 일부 건너 뛰고 복사 

input 파일에서 처음부터가 아닌 몇 블록을 건너 뛴 다음에 복사하고 싶을때는 skip 옵션을 사용하시면 됩니다. 

# dd if=size1024.bin  of=size512.bin bs=128 skip=1 count=1
  • skip : input 파일에서 건너 뛸 블록의 갯수를 의미합니다. 

위의 예에서는 블록 사이즈를 128바이트로 지정한뒤 한 블록을 건너 뛰어서 1개의 블록을 복사하는 예입니다.

 

블록의 갯수가 아니고 skip을 단순히 바이트 단위로 건너 뛰고 싶을 경우 iflag에 skip_bytes를 지정하면 됩니다.

# dd if=size1024.bin of=size512.bin skip=128 iflag=skip_bytes bs=128 count=1
  • iflag=skip_bytes : skip을 블록 단위가 아닌 바이트 단위로 인식하여 input의 내용을 건너 뜁니다. 

그래서 위의 두 예제는 같은 결과를 보여줍니다. 

 

사용법5 : output 파일의 내용을 일부 건너 뛰어 복사

skip옵션과 마찬가지로 output 파일의 블록을 건너 뛸 수도 있습니다. seek 옵션을 사용하면 됩니다. 

# dd if=size1024.bin of=size512.bin bs=128 seek=1 count=1
  • seek : 건너 뛸 output 파일의 블록 갯수를 지정합니다.

위의 예에서는 블록의 크기가 128바이트이며, size1024.bin의 블록 하나를 복사합니다. 이때 seek=1을 사용해 size512.bin의 블록 하나를 건너 뛴 자리에 복사가 됩니다.

 

역시 seek을 블록 단위가 아닌 바이트 단위로 인식시키려면 oflag에 seek_bytes를 지정하면 됩니다. 

# dd if=size1024.bin of=size512.bin bs=128 seek=128 count=1 oflag=seek_bytes
  • oflag=seek_bytes : seek을 블록 단위가 아닌 바이트 단위로 변경합니다. 

위 두 예제는 같은 결과를 보여줍니다.

 

사용법6 : 출력 파일이 존재할 때만 실행

출력 파일이 있어야만 실행해야할 경우에는 아래와 같은 명령어를 사용하면 됩니다. 

# dd if=size1024.bin of=size512.bin conv=nocreat
dd: failed to open 'size512.bin': No such file or directory
  • conv=nocreat : output 파일이 있을 경우에만 dd 명령어를 수행합니다. 

만약 파일이 존재하지 않는다면 위의 결과와 같이 output 파일이 존재하지 않는다는 오류를 발생시키고 명령어를 수행하지 않습니다.

 

dd명령은 이정도만 사용하면 무난하게 리눅스에서 사용하실 수 있을 겁니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

Datagram Socket 통신

Stream Socket은 연결 지향형이고 Datagram은 비연결형의 Socket 통신 방법입니다. 신뢰성은 그만큼 떨어지지만 단순하고 빠른 전송을 위한 것으로 UDP 프로토콜을 사용합니다. Stream Socket과는 다르게 Datagram 소켓을 사용하여 간단히 Server - Client 통신 소스코드를 짜보도록 합시다. Stream Socket은 서버와 클라이언트 사이에 연결을 맺어주느라 소스 코드의 길이가 길지요. 지금 볼 datagram socket은 이러한 연결(Connection) 과정이 생략이 됩니다.

그러니까 Stream Socket과는 다르게 listen, accept를 datagram socket에서 사용할 필요가 없어졌습니다. 그래서 다행이라고 할까요? Stream socket보다는 코드의 길이가 짧습니다.

서버 코드

아래에서 구현한 소스코드는 client의 메시지를 받아서 그대로 client쪽으로 돌려주는 echo 서버의 소스코드입니다.

//dgram-server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 12346
#define BUF_SIZE 128

void err_exit(const char *err){
        perror(err);
        exit(1);
}

int main(void){
        int socket_fd, n;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        char buffer[BUF_SIZE] = {0,};

        //Datagram socket 생성
        socket_fd=socket(PF_INET,SOCK_DGRAM,0);
        if(socket_fd < 0) err_exit("socket error ");

        //server ip와 bind하기 위해 주소 설정
        memset(&host_addr, 0, sizeof(host_addr));
        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT); 
        host_addr.sin_addr.s_addr=0;    //자신의 주소로 자동으로 설정 

        //socket과 host_addr과 bind
        if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
                err_exit("bind error ");

        //Client의 메시지를 그대로 다시 돌려준다. 
        memset(&client_addr, 0, sizeof(struct sockaddr));
        while(1){
                size = sizeof(struct sockaddr);

                //accept에서 client_addr을 얻어온 stream socket방식과 다르게
                //accept가 없는 datagram socket은 
                //recvfrom에서 client 정보를 얻어올 수 있다. 
                n = recvfrom(socket_fd, buffer, BUF_SIZE, 0,
                                (struct sockaddr*)&client_addr, &size);

                if(n < 0) err_exit("recvfrom error ");

                //Client의 접속 정보를 출력
                printf("Client Info : IP %s, Port %d\n", 
                        inet_ntoa(client_addr.sin_addr),
                        ntohs(client_addr.sin_port));


                //quit라는 메시지를 받으면 종료 
                if(!strcmp(buffer,"quit") || n == 0) break;

                //보낼 client_addr 객체까지 있어야 전달 가능
                n = sendto(socket_fd, buffer, n, 0,
                                (struct sockaddr*)&client_addr, sizeof(struct sockaddr));
                if(n < 0)err_exit("send error ");

        }

        shutdown(socket_fd, SHUT_RDWR);
        return 0;

}

 

이전 stream socket과 다른 점이 있다면, SOCK_STREAM이 아닌 SOCK_DGRAM을 사용한다는 점입니다. 

socket_fd=socket(PF_INET,SOCK_DGRAM,0);

stream socket은 send, recv를 사용하였는데, Datagram socket에서는 sendto, recvfrom을 사용하는 것을 알 수 있네요.

이 두 함수들의 끝 2개는 sockaddr의 구조체와 크기입니다. Server는 accept과정이 없기 때문에 client의 주소를 알 방법이 없습니다. 그래서 다시 echo할 주소가 없다는 거죠. recvfrom은 메시지를 받을때 송신자의 주소를 가져오기 위해서 사용합니다. sendto도 마찬가지로 보낼 곳을 지정할때 사용합니다. 이 주소 정보가 없다면 아래와 같은 관계가 성립됩니다.

send(sockfd, buf, len, flags) -> sendto(sockfd, buf, len, flags, NULL, 0)
recv(sockfd, buf, len, flags) ->  recvfrom(sockfd, buf, len, flags, NULL, NULL)

 

클라이언트 코드

이에 대응하는 client의 소스코드입니다. 

//dgram-client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

#define BUF_SIZE 128

void err_exit(const char *err){
        perror(err);
        exit(1);
}

int main(int argc, char *argv[])
{
        int sockfd = 0, n = 0;
        uint16_t port;
        char buffer[BUF_SIZE] = {0,};
        struct sockaddr_in server_addr;
        int size; 

        if(argc != 3){
                printf("\n Usage: %s server_ip port \n",argv[0]);
                return 1;
        } 

        port = atoi(argv[2]);

        //Internet용 socket(Datagram)을 연다.
        if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
                err_exit("socket error ");

        //server_addr의 구조체를 깔끔히 0으로 도배 
        memset(&server_addr, 0, sizeof(server_addr)); 
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port); 
        //127.0.0.1은 본인의 IP이다. 
        inet_aton(argv[1], (struct in_addr*)&server_addr.sin_addr);


        while(1){
                while(1){
                        printf("message:");
                        fgets(buffer, BUF_SIZE, stdin);
                        if(strlen(buffer) >= 100)
                                printf("message too large\n");
                        else break;

                };
                // \n을 \0로 처리 
                buffer[strlen(buffer)-1] = '\0';

                size = sizeof(struct sockaddr);
                //서버에게 메시지 전송
                //connect 콜이 없어서 server ip로 직접 보낸다.
                n = sendto(sockfd, buffer, strlen(buffer)+1, 0,
                                (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
                
                if(n < 0) err_exit("send error ");
                if(!strcmp(buffer, "quit")) break;

                //서버로부터 에코된 메시지를 받음
                n = recvfrom(sockfd, buffer, BUF_SIZE, 0,
                                (struct sockaddr*)&server_addr, &size);

                if(n < 0) err_exit("recv error ");

                printf("%s\n", buffer);


        }

        shutdown(sockfd, SHUT_RDWR);
        return 0;
}

결과 화면

Server Client
# ./server
Client Info : IP 127.0.0.1, Port 60112
Client Info : IP 127.0.0.1, Port 60112
Client Info : IP 127.0.0.1, Port 60112
# ./client 127.0.0.1 12346
message:hello datagram server~~
hello datagram server~~
message:good good
good good
message:quit

 

여기까지 간단하게 UDP를 사용하는 server - client 통신 구현을 해보았습니다. 여러 클라이언트를 받기 위해서는 다중 프로세스, 다중 쓰레드, 다중입출력을 활용해야겠죠. 다중입출력 방식 중 하나인 epoll에 관한 설명도 저의 블로그에 있으니 확인해보세요.

https://reakwon.tistory.com/236

 

[LINUX] epoll의 개념과 이를 활용한 다중입출력 방식의 서버, 클라이언트

epoll 뿐만 아닌 다중 입출력의 설명과 코드를 아래의 note에서 확인하실 수 있습니다. https://reakwon.tistory.com/233 리눅스 프로그래밍 note 배포 티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포

reakwon.tistory.com

 

반응형
블로그 이미지

REAKWON

와나진짜

,

epoll 뿐만 아닌 다중 입출력의 설명과 코드를 아래의 note에서 확인하실 수 있습니다. 

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

epoll

epoll은 poll과 비슷한 방식으로 동작하지만 전체 파일 디스크립터가 아닌 이벤트가 발생한 객체만 되돌려줍니다. 그리고 두 가지 이벤트 트리거 방식을 선택하여 동작할 수 있는데, Level Trigger 방식과 Edge Trigger 방식입니다. 이 둘을 줄여서 LT, ET라고 하겠습니다. 

이번 글에서는 epoll에 대한 개념을 설명한 후에 Level Trigger 방식의 에코 서버, Edge Trigger 방식의 에코서버의 예제코드를 작성해보도록 하겠습니다. 서버의 소스코드나 클라이언트 소스코드가 길게 느껴질 수 있는데, 한줄 한줄 읽어보면 진짜 별거없습니다!

Level Trigger

LT방식은 어떤 일이 일어났을 때 상태에 따라서 트리거를 지속 시킵니다. 예를 들어서 입력 버퍼가 채워져있는 상태를 1, 아닌 상태를 0으로 놓고, 입력 버퍼가 채워져있는 상태는 계속 1인 상태이기 때문에 이벤트가 지속된다는 뜻입니다.

위의 그림에서도 볼 수 있듯이 1인 상태는 버퍼가 채워져있는 상태로 정의하고, 빨간색 부분에 대해서 지속적으로 이벤트 발생을 알려줍니다.

 

Edge Trigger

반면 ET방식은 사건이 발생한 시점에 딱 한번 트리거 됩니다. 그러니까 버퍼가 채워지는 그 시점에만 이벤트가 발생한다는 뜻입니다.

이러한 LT, ET 방식인지에 따라서 잠시 후에 나올 epoll_wait 함수가 다음 이벤트까지 기다리는 시점이 달라집니다. 이제 epoll을 다루기 위한 세가지 함수를 소개합니다.

 

1. epoll_create 

#include <sys/epoll.h>
int epoll_create(int size);

epol l객체를 생성합니다. 커널 2.6.8 버전부터 이 size라는 인자는 무시되며 그냥 0보다 큰 값으로 설정만 해주면 됩니다. 실패시에는 -1을 반환하고 정상 반환일때는 0보다 큰 값이 반환됩니다. 이 반환 값을 파일 디스크립터입니다. 그래서 close로 닫을 수 있습니다. 이렇게 나오는 epoll의 파일 디스크립터는 제어, 이벤트 대기시에 사용이 됩니다.

 

2. epoll_ctl

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 함수는 epfd라는 epoll 파일 디스크립터에의해 참조되는 관심 리스트(interest list)에 fd를 더할지, 변경할지, 삭제할지를 도와주는 함수입니다. event는 그 fd에 대해서 관심있게 주시할 event를 설정해줍니다. poll과 유사합니다. event에는 아까 소개한 ET 방식도 설정할 수 있습니다. operation을 뜻하는 op는 아래의 표와 같이 세가지가 있습니다.

Op 설명
EPOLL_CTL_ADD epfd의 관심 리스트에 entry를 더해줍니다. entry는 관심있게 볼 파일 디스크립터 fd와 주시할 event를 포함한 개념이라고 보시면 됩니다.
EPOLL_CTL_MOD 관심 리스트의 fd와 연관된 세팅을 변경합니다. 여기서 fd는 전달받은 인자인 event로 새롭게 세팅 됩니다.
EPOLL_CTL_DEL 관심 리스트에 fd를 등록 해제합니다. 여기서 인자 event는 무시되니까 NULL을 전달하면 됩니다.

 

epoll_event 구조체는 아래와 같이 정의되어있습니다.

typedef union epoll_data {
        void        *ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
} epoll_data_t;

struct epoll_event {
        uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
};

 

events의 설정할 수 있는 이벤트들은 poll과 비슷합니다.

Event 설명
EPOLLIN 데이터를 읽을 수 있는 event를 설정합니다. fd에 대해서 데이터를 읽을 수 있을 때 event가 발생됩니다.
EPOLLOUT 데이터를 쓸 수 있는 event를 설정합니다. 데이터를 쓸 수 있을때 event가 발생됩니다.
EPOLLRDHUP 커널 2.6.17부터 생긴 이벤트인데, 스트림 소켓이 커넥션이 닫혔거나 shutdown 됐을때 이벤트를 발생시킵니다.
EPOLLPRI POLLPRI와 같아 high priority 자료를 바로 읽을 수 있는 이벤트입니다.
EPOLLERR 파일 디스크립터에 에러가 발생했을때 이벤트가 발생됩니다. 예외적인 이벤트이므로 이러한 이벤트는 사용자가 설정할 필요없습니다.
EPOLLHUP 파일 디스크립터가 끊겼을 때 발생합니다. 역시 사용자가 일부러 이벤트를 지정할 필요없습니다.
EPOLLET Edge Trigger 방식으로 이벤트 트리거 방식을 변경시킵니다. epoll은 기본적으로 Level Trigger 방식을 사용하고 있습니다.
EPOLLONESHOT 파일 디스크립터에 대한 이벤트를 한번만 발생시키고자 할 때 이 이벤트를 지정하면 됩니다.

 

3. epoll_wait

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

epfd에 대해서 이벤트가 발생함을 기다리는 역할을 합니다. Event에는 발생한 event들이 있습니다. 최대 기다릴 events수를 maxevents에 넣을 수 있습니다. Timeout에 따라서 epoll_wait이 계속 기다릴지, 특정 시간 동안 기다릴지, 바로 반환할 지를 정해줄 수 있습니다.

  • timeout = -1 : 이벤트 발생시까지 무한히 대기합니다.
  • timeout = 0 : 곧 장 반환합니다.
  • timeout > 0 : ms만큼 대기하다가 반환됩니다.

 

예제 코드 - Level Trigger Epoll

아래는 Level Trigger 방식의 epoll 에코 서버의 예제입니다.

server 

//epoll_server-level.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>

#FD_MAX 1024
#define PORT 12346
#define BUF_SIZE 1

const char *welcome_message = "Welcome!\n";

void err_exit(const char *err){
        perror(err);
        exit(1);
}

void clear_fd(const int epoll_fd, const int fd){
        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
        close(fd);
}
int main(void){
        int socket_fd, accepted_fd;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        int epoll_fd, i, n, ret; 
        char buffer[BUF_SIZE] = {0,};
        struct epoll_event events[FD_MAX];
        int pos = 0;

        //STREAM socket 생성
        socket_fd=socket(PF_INET,SOCK_STREAM,0);
        if(socket_fd < 0) err_exit("socket error ");

        //server ip와 bind하기 위해 주소 설정
        memset(&host_addr, 0, sizeof(host_addr));
        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT); 
        host_addr.sin_addr.s_addr = 0;  //자신의 주소로 자동으로 설정 

        //socket과 host_addr과 bind
        if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
                err_exit("bind error ");

        //접속 대기
        if(listen(socket_fd,3) < 0)
                err_exit("listen error ");

        for(i = 0; i < FD_MAX; i++)
                events[i].data.fd = -1;


        //그냥 0보다 크면 된다.
        epoll_fd = epoll_create(1024); 
        if(epoll_fd < 0) err_exit("epoll_create error ");

        struct epoll_event event;
        event.data.fd = socket_fd;
        event.events = EPOLLIN;

        if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event) < 0)
                err_exit("epoll_ctl error ");

        while(1){

                ret = epoll_wait(epoll_fd, events, FD_MAX, -1);
                //-1은 이벤트가 발생할때까지 무한정 대기
                if(ret == -1) err_exit("epoll_wait error ");
                //ret는 이벤트가 발생한 entry의 갯수, events는 발생한 events의 배열이 저장된다.
                for(i = 0; i < ret; i++){
                        //accept할 것이 있는가?
                        if(events[i].data.fd == socket_fd && events[i].events & EPOLLIN){
                                size = sizeof(struct sockaddr_in);

                                //Client가 connect할때까지 기디린다. 
                                accepted_fd = 
                                        accept(socket_fd,(struct sockaddr *)&client_addr,&size);

                                if(accepted_fd  < 0)
                                        err_exit("accept error ");

                                struct epoll_event client;
                                client.data.fd = accepted_fd;
client.events = EPOLLIN;


                                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accepted_fd, &client) < 0)
                                        err_exit("epoll_ctl error ");
                                //Client의 접속 정보를 출력하고 접속 잘됐다고 메시지 전송
                                printf("Client Info : IP %s, Port %d\n", 
                                                inet_ntoa(client_addr.sin_addr),
                                                ntohs(client_addr.sin_port));

                                n = send(accepted_fd, welcome_message, strlen(welcome_message), 0);
                                if(n < 0) err_exit("send error ");
                                continue;
                        } 

                        if(events[i].events & EPOLLIN){

                                n = recv(events[i].data.fd, &buffer[pos], BUF_SIZE, 0);

                                if(n < 0) err_exit("recv error ");

                                //n == 0일 경우에 epoll_fd 관심 리스트에서 비워준다. 
                                if(n == 0) {
                                        clear_fd(epoll_fd, events[i].data.fd);
                                        pos = 0;
                                        continue;
                                }

                                if(buffer[pos] == '\0'){
                                        printf("rcv msg : %s\n", buffer);
                                        //quit라는 메시지를 받으면 종료
                                        if(!strcmp(buffer,"quit")){
                                                clear_fd(epoll_fd, events[i].data.fd);
                                                pos = 0;
                                                continue;
                                        }

                                        n = send(events[i].data.fd, buffer, pos, 0);
                                        if(n < 0) err_exit("send error ");
                                        pos = 0;
                                } else 
                                        pos++;
                        }

                }

        }
        printf("end\n");
        shutdown(socket_fd, SHUT_RDWR);

        return 0;

}

 

client

//client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

#define BUF_SIZE 128

void err_exit(const char *err){
        perror(err);
        exit(1);
}

int main(int argc, char *argv[])
{
        int sockfd = 0, n = 0;
        uint16_t port;
        char buffer[BUF_SIZE] = {0,};
        struct sockaddr_in server_addr; 

        if(argc != 3){
                printf("\n Usage: %s server_ip port \n",argv[0]);
                return 1;
        } 

        port = atoi(argv[2]);

        //Internet용 socket을 연다.
        if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
                err_exit("socket error ");

        //server_addr의 구조체를 깔끔히 0으로 도배 
        memset(&server_addr, 0, sizeof(server_addr)); 
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port); 
        //127.0.0.1은 본인의 IP이다. 
        inet_aton(argv[1], (struct in_addr*)&server_addr.sin_addr);

        //client는 서버의 ip, port, protocol 설정후 connect로 서버에게
        //바로 연결한다.
        if(connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
                err_exit("connect error ");


        while(1){
                //서버로부터 에코된 메시지를 받음
                n = recv(sockfd, buffer, BUF_SIZE, 0);

                if(n < 0) err_exit("recv error ");

                printf("%s\n", buffer);

                while(1){
                        printf("message:");
                        fgets(buffer, BUF_SIZE, stdin);
                        if(strlen(buffer) >= 100)
                                printf("message too large\n");
                        else break;

                };
                // \n을 \0로 처리 
                buffer[strlen(buffer)-1] = '\0';

                //서버에게 메시지 전송
                n = send(sockfd, buffer, strlen(buffer)+1, 0);
                if(n < 0) err_exit("send error ");

        }

        shutdown(sockfd, SHUT_RDWR);
        return 0;
}

 

Level Trigger 방식의 결과화면입니다.

Server
# ./server
Client Info : IP 127.0.0.1, Port 42872
rcv msg : hello epoll server
Client Info : IP 127.0.0.1, Port 46286
rcv msg : This is level trigger
rcv msg : good bye~~~~~
rcv msg : quit
rcv msg : GOOD BYE ^C
Client1 Client2
# ./client 127.0.0.1 12346
Welcome!
message:hello epoll server
hello epoll server
message: GOOD BYE
GOOD BYE
message:^C
# ./client 127.0.0.1 12346
Welcome!
message:This is level trigger
This is level trigger
message: good bye~~~~~
good bye~~~~~
message:quit
quit
message:
message:

 

예제 코드 - Edge Trigger Epoll

위 LT방식은 ET방식으로 바꿔볼까요? event에다가 EPOLLET를 추가하시면 ET방식으로 동작하게 됩니다. accepted_fd를 EPOLLET event를 같이 추가해서 실행해보세요.

struct epoll_event client;
client.data.fd = accepted_fd;
client.events = EPOLLIN | EPOLLET;

그 후 실행하게 되면 에코가 되지 않습니다.

Server
# ./server
Client Info : IP 127.0.0.1, Port 43596
Client Info : IP 127.0.0.1, Port 43600
Client1 Client2
# ./client 127.0.0.1 12346
Welcome!
message:Hello! epoll ET server
# ./client 127.0.0.1 12346
Welcome! message:Hell;;

 

왜 이런 결과가 나올까요?

우리가 구현한 방식은 한 글자씩 버퍼에서 읽어옵니다. 이는 epoll_wait이 반환된 후에 읽을 수가 있죠. LT방식일때는 버퍼에 데이터가 남아있으면 epoll_wait에 의해서 반환되게 됩니다. 하지만 ET방식은 버퍼가 채워지는 이벤트 순간 한번만 epoll_wait이 반환되기 때문에 버퍼에서 한글자만 읽고 대기하게 되는 겁니다.

그럼 여기서 질문, select와 poll은 어떤 방식을 쓰고 있는 걸까요?

우리가 현상을 보게 되면 버퍼가 남이있다면 select와 poll은 return됩니다. 이런 결과를 봐서 우리는 select와 poll은 LT방식인 것을 알 수 있습니다. 이러한 문제를 해결하기 위해서는 non-blocking 방식의 read를 해야합니다.즉, 더 이상 버퍼에 남아있는 데이터가 없어서 오류를 발생할때까지 읽어야합니다. 이때 남아있는 데이터가 없으면 EAGAIN 에러를 발생하게 됩니다. 그래서 이런 방식으로 수정해야합니다.

  • 비차단 모드로 설정 

 

//NON-BLOCKING 모드로 전환
int flags = fcntl(accepted_fd, F_GETFL);
flags |= O_NONBLOCK;
if(fcntl(accepted_fd, F_SETFL, flags) < 0) 
    err_exit("fcntl error ");
  • read에서 EAGAIN 오류를 만날때까지 읽기
while(1){
    n = recv(events[i].data.fd, &buffer[pos], BUF_SIZE, 0);

    if(n < 0) 
        if(errno == EAGAIN) break;
    if(n == 0) {
        clear_fd(epoll_fd, events[i].data.fd);
        pos = 0;
        break;
    }

    pos++;
}

 

전체 풀 소스코드는 아래와 같습니다. 

//epoll_server-edge.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>

#define PORT 12346
#define BUF_SIZE 1

const char *welcome_message = "Welcome!\n";

void err_exit(const char *err){
        perror(err);
        exit(1);
}

void clear_fd(const int epoll_fd, const int fd){
        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
        close(fd);
}
int main(void){
        int socket_fd, accepted_fd;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        int epoll_fd, i, n, ret; 
        char buffer[BUF_SIZE] = {0,};
        struct epoll_event events[FD_MAX];
        int pos = 0;

        //STREAM socket 생성
        socket_fd=socket(PF_INET,SOCK_STREAM,0);
        if(socket_fd < 0) err_exit("socket error ");

        //server ip와 bind하기 위해 주소 설정
        memset(&host_addr, 0, sizeof(host_addr));
        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT); 
        host_addr.sin_addr.s_addr = 0;  //자신의 주소로 자동으로 설정 

        //socket과 host_addr과 bind
        if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
                err_exit("bind error ");

        //접속 대기
        if(listen(socket_fd,3) < 0)
                err_exit("listen error ");

        for(i = 0; i < FD_MAX; i++)
                events[i].data.fd = -1;


        //그냥 0보다 크면 된다.
        epoll_fd = epoll_create(1024); 
        if(epoll_fd < 0) err_exit("epoll_create error ");

        struct epoll_event event;
        event.data.fd = socket_fd;
        event.events = EPOLLIN;
        //event.events = EPOLLIN | EPOLLET;

        if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event) < 0)
                err_exit("epoll_ctl error ");

        while(1){

                ret = epoll_wait(epoll_fd, events, FD_MAX, -1);
                //-1은 이벤트가 발생할때까지 무한정 대기
                if(ret == -1) err_exit("epoll_wait error ");
                //ret는 이벤트가 발생한 entry의 갯수, events는 발생한 events의 배열이 저장된다.
                for(i = 0; i < ret; i++){
                        //accept할 것이 있는가?
                        if(events[i].data.fd == socket_fd && events[i].events & EPOLLIN){
                                size = sizeof(struct sockaddr_in);

                                //Client가 connect할때까지 기디린다. 
                                accepted_fd = 
                                        accept(socket_fd,(struct sockaddr *)&client_addr,&size);

                                if(accepted_fd  < 0)
                                        err_exit("accept error ");

                                struct epoll_event client;
                                client.data.fd = accepted_fd;
                                client.events = EPOLLIN | EPOLLET;

                                //NON-BLOCKING 모드로 전환
                                int flags = fcntl(accepted_fd, F_GETFL);
                                flags |= O_NONBLOCK;
                                if(fcntl(accepted_fd, F_SETFL, flags) < 0) 
                                        err_exit("fcntl error ");


                                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accepted_fd, &client) < 0)
                                        err_exit("epoll_ctl error ");
                                //Client의 접속 정보를 출력하고 접속 잘됐다고 메시지 전송
                                printf("Client Info : IP %s, Port %d\n", 
                                                inet_ntoa(client_addr.sin_addr),
                                                ntohs(client_addr.sin_port));

                                n = send(accepted_fd, welcome_message, strlen(welcome_message), 0);
                                if(n < 0) err_exit("send error ");
                                continue;
                        } 

                        if(events[i].events & EPOLLIN){
                                while(1){
                                        n = recv(events[i].data.fd, &buffer[pos], BUF_SIZE, 0);

                                        if(n < 0) 
                                                if(errno == EAGAIN) break;
                                        if(n == 0) {
                                                clear_fd(epoll_fd, events[i].data.fd);
                                                pos = 0;
                                                break;
                                        }

                                        pos++;
                                }

                                //quit라는 메시지를 받으면 종료
                                if(!strcmp(buffer,"quit") || n == 0){
                                        clear_fd(epoll_fd, events[i].data.fd);
                                        pos = 0;
                                        continue;
                                }
                                printf("rcv msg : %s\n", buffer);

                                n = send(events[i].data.fd, buffer, pos, 0);
                                if(n < 0) err_exit("send error ");
                                pos = 0;
                        }

                }

        }
        printf("end\n");
        shutdown(socket_fd, SHUT_RDWR);

        return 0;

}

Server
# ./server
Client Info : IP 127.0.0.1, Port 34820
Client Info : IP 127.0.0.1, Port 34832
rcv msg : hello! epoll ET server
rcv msg : HI HI ~~~~
rcv msg : BYE!!!
rcv msg : edge trigger~~~~
rcv msg : GOOD
Client Info : IP 127.0.0.1, Port 60190
rcv msg : I'm back
rcv msg : Bye
Client1 Client2
# ./client 127.0.0.1 12346
Welcome!
message:hello! epoll ET server
hello! epoll ET server
message:edge trigger~~~~
edge trigger~~~~
message:GOOD
GOOD
message:^C
# ./client 127.0.0.1 12346
Welcome!
message:I'm back
I'm back
message:Bye Bye
message:^C
# ./client 127.0.0.1 12346
Welcome!
message:HI HI ~~~~
HI HI ~~~~
message:BYE!!! BYE!!!
message:quit
quit
message:
message:

 

이상으로 epoll에 관한 설명을 마칩니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

시큐어부트(Secure Boot)

시큐어부트(Secure Boot)는 말 그대로 안전한 부팅을 의미합니다. 시큐어 부팅은 ROM으로부터 시작되는 부팅의 시작부터 파일 시스템이 얹어지는 부팅 완료까지 안전하게 부팅을 하는 절차입니다. 주로 세 단계의 인증을 거치게 됩니다. 맨 처음 부팅의 시작인 부트로더들의 인증, 그리고 부트로더에 의해 커널 이미지가 올라오게 되면 커널 이미지를 인증, 이후 커널에 의해 파일 시스템이 마운트되기 전에 파일 시스템의 인증을 거치게 됩니다. 

결론은 부트 로더 인증 -> 커널 인증 -> 파일 시스템 인증 순으로 이어지게 된다는 것이죠. 

부트 로더 인증

임베디드 시스템에서 Boot Loader는 한 가지만 존재하지 않습니다. ROM에서 시작되는 부트 로더1부터 시작해 부트로더2, 부트로더 3 등이 있으며, 여기서도 부트로더 순서대로 인증하는 CoT(Chain Of Trust)라는 기술이 존재하지만 이 포스팅에는 부트로더와 다른 Firmware 이미지들을 한 꺼번에 인증하는 FIP 인증을 다룹니다.

FIP(Firmware Image Package)라고하는 펌웨어 이미지 바이너리를 인증합니다. FIP는 단순히 부트로더들의 모임이라고 생각하시면 되고, FIP를 서명한 서명값이 FIP끝에 달리게 됩니다. 이때 인증은 각 SoC 업체의 Firmware를 사용하여 인증을 하게 되는데요. NXP사의 s32g 칩의 경우에는 HSE(Hardware Security Engine)이라는 펌웨어가 그 역할을 하게 되지요. 

 

인증을 하는 경우에는 공개키가 있어야겠죠? 이 공개키는 어딘가에 저장이 되어야하는데 nxp s32g의 경우 FAT 파일 시스템을 통해서 특정 파티션에 저장하고 있습니다.

커널 인증 

부트로더 인증이 되었다면 그 이후 커널 인증을 거쳐야합니다. 커널 인증은 커널이 변조되었는지 아닌지를 판별하게 됩니다. 커널 인증을 하는 대표적인 방법은 Verified Boot라는 건데요. U-boot에서 사용하는 커널 인증 방법으로 FIT(Flattened Image Tree)를 이용합니다. 이 FIT는 아래와 같은 형식의 .its라는 파일을 가지고 생성되어 집니다. its는 FIT에 대한 정보를 갖고 있는 소스 파일입니다. 

/dts-v1/;
/ {
    description = "Verified boot FIT Image";
    #address-cells = <2>;
    images {
        kernel-1 {
            description = "FIT kernel Image";
            data = /incbin/("Image");
            type = "kernel";
            arch = "arm64";
            os = "linux";
            compression = "none";
            load =  <0x00080000>;
            entry = <0x00080000>;
            hash@1 {
                algo = "sha256";
            };
        };
        tee-1 {
            description = "Arm Trusted Firmware";
            data = /incbin/("optee.bin");
            type = "standalone";
            arch = "arm64";
            compression = "none";
            load =  <0x08400000>;
            entry = <0x08400000>;
            hash@1 {
                algo = "sha256";
            };
        };
        fdt-1 {
            description = "FIT device tree";
            data = /incbin/("fit-linux-kernel.dtb");
            type = "flat_dt";
            arch = "arm64";
            compression = "none";
            hash@1 {
                algo = "sha256";
            };
        };
    };
    configurations {
        default = "config-1";
        config-1 {
            description = "verified boot FIT configuration";
            kernel = "kernel-1";
            loadables = "tee-1";
            fdt = "fdt-1";
            signature-1 {
                algo = "sha256,rsa2048";
                key-name-hint = "dev";
                sign-images = "fdt", "kernel", "loadables";
            };
        };
    };
};

마지막 configurations에서 signature-1을 보시면 FIT 인증을 위한 정보들이 저장이 됩니다. 이 its를 가지고 mkimage라는 u-boot의 툴을 통해서 FIT를 아래와 같은 방법으로 만들어냅니다.

./u-boot/tools/mkimage -f fit-image.its -K u-boot.dtb -k keys -r image.fit
  • -f : its의 파일 이름
  • -K : 그렇다면 공개키는 어디 있을까요? 공개키는 앞서 인증한 FIP의 커널을 부팅하는 부터로더인 U-boot의 dtb에 존재합니다. 그 dtb 파일의 이름을 -K 옵션으로 정해주면 이 dtb 파일에 공개키가 삽입이 됩니다. 
  • -k : 서명할 키와 인증서가 있는 디렉토리를 정해줍니다. keys라는 디렉토리에 위치해있으며 openssl이든 다른 키 생성 툴이든 사용하여 키와 인증서를 만들어야합니다. 위의 예시에는 키에 대한 정보(key-name-hint)가 dev입니다.
  • -r : 인증이 필수라는 뜻으로 인증을 강제합니다. 이 옵션을 정해줌으로써 인증이 실패한 커널은 부팅하지 않습니다. 
  • image.fit : 최종적으로 나오는 fit 이미지의 이름을 정해줍니다. input이 아닙니다.

 

이 FIT 이미지는 U-boot의 bootm 부팅 커맨드로만 동작이 가능하며 u-boot에서 iminfo 라는 명령을 통해서 이미지의 정보를 확인할 수도 있습니다.

자세한 사용 방법은 아래를 참고하시면 될 것 같네요.

https://blog.crysys.hu/2018/06/verified-boot-on-the-raspberry-pi/

 

Verified boot on the Raspberry Pi – CrySyS Blog

This blog post, written by István Telek, is the third post in a series of blog posts on transforming the Raspberry Pi into a security enhanced IoT platform. It describes how you can implement a verified boot process on the Raspberry pi. Introduction Secur

blog.crysys.hu

 

파일 시스템 인증

자, 이제 마지막 인증 과정입니다. 바로 실제 데이터들이 존재하는 파일 시스템을 마운트하기전 파일 시스템을 인증하는 과정이 필요하게 되지요. 파일 시스템을 인증하는 대표적인 기술로 사용되는 것이 dm-verity라는 기술입니다. dm-verity는 파일 시스템 이미지의 원본 블록 을 1차로 나온 Hash 데이터를 다시 2차 Hash를 가하고, 이후 n 번의 Hash를 가하게 되면 결국 마지막 나오는 hash값을 갖고 파일 시스템의 무결성을 검증합니다. 하지만 인증을 위해서는 이 hash값을 서명해야됩니다. 결국 우리는 파일 시스템 원본 이미지 + 해쉬 블록을 미리 생성하고, root hash까지 미리 생성하여 안전하게 보관해야합니다. 이때 hash가 계산되는 모습을 보면 트리 형태로 보여지는데 이를 merkle Tree 라고합니다. 결과적으로 나온 root hash는 노출되어서는 안돼요. 부팅이 되고 마운트될때 파일 시스템의 이미지를 root hash로 만들어서 갖고 있는 root hash와 같다면 인증에 성공합니다.

https://www.timesys.com/security/dm-verity-without-an-initramfs/

한가지 중요한 것은 파일 시스템의 쓰기가 발생하면 안된다는 점입니다. 쓰기가 일어나면 인증은 다음 부팅때 당연히 실패합니다. 그래서 파일 시스템은 read-only 파일 시스템에서만 dm-verity가 사용이 가능합니다. 이 dm-verity에 관한 내용과 파일 시스템 인증에 관한 방법은 아래의 페이지에서 세세하게 다루고 있으니 이 페이지를 참고하시기 바랍니다.

https://www.timesys.com/security/dm-verity-without-an-initramfs/

 

DM-Verity Without an Initramfs - Timesys

Learn how you can implement file system verification on your embedded system without the use of an initramfs. This can significantly save boot time and storage requirements in many situations.

www.timesys.com

 

이상으로 시큐어부팅에 관한 포스팅을 마치도록 하겠습니다.

반응형

'컴퓨터 > 보안 기술' 카테고리의 다른 글

[사이버 보안] dm-verity 개념과 실습(Manual)  (1) 2023.05.18
블로그 이미지

REAKWON

와나진짜

,

저장된 사용자 ID - Saved UID

Saved UID를 이해하기 위해서는 실제 사용자 ID(Real UID)와 유효 사용자 ID(Effective UID)에 대한 이해가 깔려있어야합니다. 아직 개념이 안잡혀있다면 아래의 포스팅을 먼저 보고 오시면 좋겠습니다. 

https://reakwon.tistory.com/228

 

[리눅스] 코드로 알아보는 uid(real uid, effective uid, saved uid) 관계

아래 포스팅보다 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요. https://reakwon.tistory.com/233 리눅스 프로그래밍 note 배포 티스토리에 리눅

reakwon.tistory.com

 

Saved UID가 없다고 생각하고 Real UID와 Effective UID만 있다고 가정 해보세요. 그리고 setresuid 역시 suid를 설정하는 함수이니 없다고 가정해보세요. 권한이 확장된 프로그램에서의 euid를 권한이 확장된 euid라고 부르겠습니다. 다른 사용자의 권한을 얻은 상태에서 다시 권한을 축소 시켜야할때, 그러니까 실사용자 ruid로 돌려야할 상황이 생길 때 단순히 ruid로만 돌리면 나중에 다시 유효 사용자 권한이 필요할 때 돌아올 방법이 없습니다. 그러니까 euid, ruid를 왔다리 갔다리 유도리있게 스위칭할수가 없다는 뜻입니다.

  • setuid를 통해서 권한이 확장된 euid로 다시 바꿀 수 있을까요? 현재 ruid=euid입니다. suid는 없다고 가정했으니, ruid로만 변경될 수 있는데, 이는 권한이 확장된 euid가 아니잖아요. setuid로는 바꿀 수 없습니다.
  • seteuid 역시 마찬가지인데요. ruid 혹은 suid로 돌아갈 수 있는데 지금은 suid가 없다고 가정했으니 역시 setuid의 결과와 같습니다. 바꿀 수 없죠.  
  • setreuid도 마찬가지입니다. ruid가 현재 euid이기 때문이죠. setreuid는 현재의 ruid 혹은 euid로만 변경되는데, 지금 상황은 ruid가 euid와 같죠.

즉, 저장된 사용자 ID가 없이 ruid와 euid만 존재하면 현재 ruid = euid가 생길 때 다시 확장된 권한의 euid로 돌아갈 방법이 없다는 뜻입니다. 그래서 따로 그러한 확장된 권한의 euid를 저장해 놓아야하는데, 이를 위한 uid가 바로 saved-user id입니다.

그래서 이전 지난 포스팅에 suid가 초기의 euid와 같다는 점을 주시하라고 한겁니다. 저의 큰 그림 아시겠어요? 이제 saved라는 의미가 왜 붙었는지 아시겠죠?

예를 들어, root 권한의 프로그램에서 잠시 root 권한을 뺄 때를 생각해봅시다. 위와 같은 상황은 발생하겠죠. 아무리 root가 프로그램 사용권한을 자신의 것으로 쓰게 끔 허락했어도 특정 파일에 대한 접근 권한을 막을 상황이 생길겁니다. 이런 경우 seteuid를 사용자의 ruid로 돌려줍니다. 현재 ruid = 실 사용자 id, 현재 euid = 실 사용자 id 그러다가 그 후 다시 root 권한으로 작업해야할 때는 seteuid를 다시 root로 돌려야하겠죠. 그런데 현재 상황에서 ruid는 실 사용자의 id, 그리고 euid도 실 사용자 id인데, 어떻게 돌릴 수 있죠? suid는 아까 봤듯이 euid와 같았죠. 네, suid 덕분에 다시 유효사용자 id를 root로 돌릴 수 있게 됩니다.  

아래의 코드는 root.txt라는 root만든 텍스트 파일을 euid를 변경해가며 읽는 소스 코드입니다. 실행하면서 어떤 현상이 발생하는지 관찰해보면 suid가 왜 쓰이는지 알 수 있을 겁니다.

//savedUID.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

void readfile(){
        int n;
        char buf[32]={0,};
        int fd = open("root.txt", O_RDWR);
        if(fd < 0){
                printf("open error(%s)\n", strerror(errno));
                return;
        }


        n = read(fd, buf, 32);
        if(n < 0){
                printf("read error(%s)\n", strerror(errno));
                close(fd);
                return;
        }
        printf("%s", buf);
        close(fd);


}
void pr_resuid(){
        int ruid, euid, suid;
        if(getresuid(&ruid, &euid, &suid) < 0){
                printf("setresuid error\n");
                exit(0);
        }
        printf("ruid:%d, euid:%d, suid:%d\n",
                ruid, euid, suid);
}
int main(){

        printf("초기 uid\n");
        pr_resuid();

        printf("file 읽기 시도 > ");
        readfile();
        printf("\n");

        printf("euid를 %d로 전환\n", getuid());
        if(seteuid(getuid()) < 0){
                printf("seteuid error\n");
                return 1;
        }
        pr_resuid();

        printf("file 읽기 시도 > ");
        readfile();
        printf("\n");

        printf("euid를 root로 전환\n");
        seteuid(0);

        printf("file 읽기 시도 > ");
        readfile();
        pr_resuid();
}

우선 root의 유효사용자 id를 root로 돌려야하기 때문에 권한을 줘야겠군요. gcc 경고에 대한 문구는 가볍게 쌩까도록 합시다. 아참, 그리고 root권한의 파일을 하나 만들어야겠네요. root.txt를 읽어야하잖아요.

root# gcc savedUID.c 
savedUID.c: In function ‘pr_resuid’:
savedUID.c:32:12: warning: implicit declaration of function ‘getresuid’; did you mean ‘setreuid’? [-Wimplicit-function-declaration]
   32 |         if(getresuid(&ruid, &euid, &suid) < 0){
      |            ^~~~~~~~~
      |            setreuid
root# chmod u+s a.out
root# echo "this is a file made by root" > root.txt

 

계정을 ubuntu로 전환하고 파일을 실행하면 어떤 결과가 나올까요?

root# su ubuntu
ubuntu$ ./a.out 
초기 uid
ruid:1000, euid:0, suid:0
file 읽기 시도 > this is a file made by root

euid를 1000로 전환
ruid:1000, euid:1000, suid:0
file 읽기 시도 > open error(Permission denied)

euid를 root로 전환
file 읽기 시도 > this is a file made by root
ruid:1000, euid:0, suid:0

 

euid를 잠시 ubuntu로 전환하고 파일을 읽을 땐 파일을 읽을 수 없습니다. 여기서 알 수 있는 것은 permission denied로 권한이 축소되었음을 알 수 있습니다. 그 후에는 euid를 root로 전환하여 파일을 읽을 수 있게 되었습니다. euid를 0으로 되돌릴 수 있는 이유는 suid가 0이기 때문이고 만약 suid 마저도 다른 uid로 변경되었다면 seteuid는 에러를 발생시킵니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

환경 변수

컴퓨터 시스템에서 사용되는 동적인 값을 저장하는 변수, 프로그램들이 시스템 설정과 상호작용하는 데 사용됩니다. 환경변수는 텍스트 형식으로 저장되며, 특정 이름과 그에 해당하는 값으로 구성됩니다.

이름 =  

시스템 전체에서 공유되거나 특정 프로세스 내에서만 유효한 변수들이 있을 수 있습니다. 환경변수는 주로 다음과 같은 용도로 사용됩니다.

  1. 시스템 설정 및 구성 정보 저장 시스템의 설정 정보를 저장하여 다른 프로그램들이 해당 정보를 활용할 수 있게 합니다. 예를 들어, 시스템의 언어, 시간대, 기본 디렉토리 등을 환경변수로 설정하여 시스템 전반에 적용할 수 있습니다. 
  2. 프로그램 실행 시 동작 옵션 설정 프로그램이 동작할 때 필요한 옵션을 환경변수로 설정하여 해당 프로그램이 이를 인식하고 적용하게 할 수 있습니다. 이를 통해 특정 프로그램의 동작을 조정하거나 사용자 맞춤 설정을 지원할 수 있습니다.
  3. 실행 환경 설정 프로그램이 실행되는 환경을 설정하는 데 사용됩니다. 예를 들어, 실행 파일들을 찾는 경로를 설정하거나 라이브러리의 위치를 지정하는데 환경변수를 활용할 수 있습니다.

 우리는 현재 쉘에서 환경 변수를 설정한적은 없죠. 그런데 로그인 할 때 기본적으로 설정되어 있는 환경 변수들이 있습니다(여기서는 bash 쉘만 보겠습니다.) printenv 명령(혹은 env)을 통해서 현재 쉘에 어떤 환경변수가 설정되어있는지 확인해볼까요?

# printenv
SHELL=/bin/bash
PWD=/root
LOGNAME=ubuntu
//...
HOME=/root
LANG=en_US.UTF-8
//...
USER=ubuntu
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
MAIL=/var/mail/root
SSH_TTY=/dev/pts/0

제가 있는 현재 bash 쉘의 환경변수는 이렇게 설정되어있습니다. 환경 변수도 변수이기 때문에 그 값을 변경하거나 없애거나 할 수 있습니다. 그런데 보통 기본적으로 설정되어있는 환경 변수는 건드리지 않는 편이 좋습니다. 예를 들어 PATH를 건드리게 되면 명령어 실행을 못할 수도 있습니다. 여기서는 알아두면 좋을 환경 변수들을 설명하고 환경 변수를 설정하는 방법과 프로그램에서 어떻게 환경 변수를 읽어오고 설정하는지 확인해보도록 하겠습니다.

1. 일반적인 환경 변수들

아래의 환경 변수들은 기본적으로 현재 쉘에 설정되어있을 수 있는 환경 변수들입니다.

환경변수  설명
USER 현재 로그인한 사용자입니다. UID(숫자)가 아닌 계정명으로 나타납니다.
HOME 현재 사용자의 홈 디렉토리입니다. 로그인하면 이 디렉토리로 자동으로 위치가 됩니다.
EDITER 사용할 기본 파일 편집기입니다. 터미널에 edit를 입력할 때 사용할 편집기입니다.
LANG 현재 로컬 언어 설정을 보여줍니다.
TERM 현재 터미널 에뮬레이션입니다.
MAIL 현재 사용자의 메일이 저장되는 위치입니다.
PATH  명령어를 실행할 때 검색할 디렉토리들입니다. 여러 디렉토리가 있을 수 있기 때문에 :(콜론)으로 구분이 됩니다.
여러분이 명령어를 아무데서나 칠 수 있는 이유는 바로 이 PATH변수 때문입니다. 이 변수가 없다면 ls 명령을 칠 때 전체 경로를 넣어서 /bin/ls를 쳐야하지만 PATH/bin 디렉토리가 있기 때문에 ls만 쳐서 명령을 칠 수 있는 것입니다.
SHELL 현재 사용자의 로그인 쉘입니다.
PWD 현재 사용자가 있는 디렉토리입니다

 

2.  환경 변수 설정

우선 일반적으로 쉘에서 단순 변수는 아래와 같이 설정할 수 있습니다.

변수명= 

그 변수를 참조할 때는 변수명 앞에 $를 사용하여 확인할 수 있습니다. 며칠전에 달라로 500만원을 환전했는데 개떡락을 하더군요 --;;

# VAL="hello, world"
# echo $VAL
hello, world

그런데 이건 이 쉘에서만 유효한 변수일 뿐이지 환경 변수는 아닙니다. printenvenv 명령을 통해서 현재 쉘에 설정된 환경 변수확인해보세요. 환경변수는 아니라는 것을 알 수 있습니다.

# printenv | grep VAL
아무 결과도 나오지 않음

대신에 현재 쉘 환경의 환경 변수를 포함한 모든 변수, 함수 등을 출력하는 set 명령을 통해 확인하면 변수가 설정되어있는 것은 알 수 있습니다.

# set | grep VAL
VAL='hello world'

그렇다면 환경 변수를 설정하려면 어떻게 해야 할까요?

 export 명령

export 명령, 일명 내보내기 명령을 통해서 현재 쉘의 환경 변수로 변수를 등록할 수 있습니다. 이때 변수명 앞 $는 생략해야합니다.

export 변수명

# VAL="hello world"
# export VAL
# printenv | grep VAL
VAL=hello world

혹은 한번에 값을 지정해서 내보낼 수도 있습니다.

export 변수명=

# export APPLE="Mac Book"
# printenv | grep APPLE
APPLE=Mac Book

이제 환경 변수로 등록되었다는 것을 알 수 있습니다.  

환경 변수나 변수를 해제하려면 unset 명령어를 통해서 해제할 수 있습니다.

unset 변수명

# printenv |grep VAL
VAL=hello world
# unset VAL
# printenv | grep VAL
# printenv | grep APPLE
APPLE=Mac Book
# unset APPLE
# printenv | grep APPLE

이런 환경 변수는 현재 수행되는 쉘이 유지되는 동안만 유효(사실 더 정확하게는 환경변수를 설정한 프로세스가 유지되는 동안 유효)합니다. 무슨 말이냐구요?

3. 환경변수의 특성

계속 주구장창 현재 쉘, 현재 쉘이라는 말을 썼는데요. 여러분들이 놓치실까봐 초록색으로 표시했습니다. 그림으로 보면 더욱 이해가 빠르실텐데요.  

처음 로그인 쉘인 (1) /bin/bash에서 export를 이용해서 HELLO를 환경 변수로 등록하고 sh로 새로운 쉘을 시작합니다. 이때 (2)/bin/sh로 실행되는 환경이 바뀌게 됩니다. 여기서도 export를 이용해서 WORLD를 환경 변수로 등록하고 다시 새로운 (3)bash 쉘을 실행시켜서 실행 환경을 바꿉니다. 여기서 BYE라는 환경 변수를 등록합니다.

그러면 맨 오른쪽 (3)bash 쉘을 사용하고 있을때 HELLO, WORLD, BYE라는 환경변수는 다 살아있을까요? , 살아있습니다. 맨처음 (1)/bin/bash, 그 다음 (2)/bin/sh도 실행중이니까요. 이제 exit으로 (3)bash를 끝내게 되어도 세 환경 변수 HELLO, WORLD, BYE는 살아있을까요? BYE(3)bashexit을 종료했기 때문에 더 이상 존재하지 않습니다. 그래서 HELLO, WORLD만 남아있게 됩니다. (2)/bin/sh를 끝냈을때, 이때 (1)/bin/bash만 수행되는 상태겠죠. 그러면 HELLO만 환경 변수로 유효하게 되는 겁니다.

아래는 테스트 결과입니다. 헷갈리지 않도록 앞에 프롬프트를 추가했습니다. 참고로 전 쉘에서 설정한 변수를 다음 쉘에서 이용하려면 export를 이용해 환경변수로 등록이 되어야 가능합니다. 그러니까 (1)/bin/bash에서 설정한 HELLO(2)/bin/shecho로 출력하려면 export되어있어야 된다는 겁니다.

(1)/bin/bash# export HELLO="hello!"
(1)/bin/bash# sh
(2)/bin/sh# export WORLD="world!"   
(2)/bin/sh# bash
(3)/bin/bash# export BYE="GOOD BYE~"
(3)/bin/bash# echo $HELLO $WORLD $BYE
hello! world! GOOD BYE~
(3)/bin/bash# exit
exit
(2)/bin/sh# echo $HELLO $WORLD $BYE
hello! world!
(2)/bin/sh# exit
(1)/bin/bash# echo $HELLO $WORLD $BYE
hello!

이런 특성을 모른다면 이런 실수 많이들 하실겁니다.  아래는 간단한 bash 쉘 스크립트를 작성한겁니다.

#!/bin/bash

#현재 프로세스 ID 출력 
echo $$

export HELLO="hello "
export WORLD="world!"
export BYE="good bye!"

echo $HELLO $WORLD $BYE

스크립트를 몰라도 맨위의 첫줄은 bash 스크립트를 알려주는 한줄이니까 그냥 그렇게 쓰는구나 묻지도 따지지지도 말고 흡수하시면 됩니다.

#!/bin/bash

그 다음 줄을 좀 눈여겨 봐야하는데, $$는 현재 프로세스의 ID를 의미합니다. 그래서 echo를 통해서 출력하는 것이구요. 그냥 간단히 현재 실행되는 프로그램의 ID라고 생각하세요.

#현재 프로세스 ID 출력 
echo $$

그리고 우리가 배운 export와 명령을 출력하는 echo가 다입니다.  위 스크립트의 의도는 $HELLO, $WORLD, $BYE를 환경 변수로 설정해서 편하게 쓰려는 의도입니다.  실행권한을 주고 실행하면 쉘 스크립트는 잘 실행되지만 스크립트가 끝난 이후 환경 변수를 불러오고자 한다면 이렇게 빈 공백만 출력이 됩니다.

# chmod 777 setmyenv.sh
# ./setmyenv.sh 
6968
hello world! good bye!
# echo $$
3196
# echo $HELLO

# echo $WORLD

# echo $BYE

그런데 제가 명령어를 수행하는 프로그램의 ID3196이고, , ./setmyenv.sh라는 쉘 스크립트가 실행되는 프로그램 ID6968로 다르네요. 결국 이런 상황입니다.

방금 전 언급한 그 상황이 발생했습니다. 그러면 새로운 실행을 낳지 않고, 현재 있는 쉘에서 그대로 스크립트를 수행하면 될 것 같습니다. 그런 목적을 달성할 명령이 바로 source 명령입니다.

 위의 스크립트를 source를 이용해서 실행해봅시다.

# source setmyenv.sh 
3196
hello world! good bye!
# echo $HELLO
hello
# echo $WORLD
world!
r# echo $BYE
good bye!
# echo $$
3196

보세요. 쉘 스크립트가 그대로 3196에서 실행이 되었죠? 그리고 환경 변수가 설정되어서 현재 쉘에서 사용할 수도 있습니다. 우리가 원하는 목적을 달성할 수 있었습니다. source는 쉘 스크립트를 현재 쉘에서 실행하게 해주는 명령어입니다.

4. 환경 변수 함수

이젠 리눅스에서 프로그램이 어떻게 환경 변수를 다룰 수 있는 지 볼까요? 환경 변수를 설정하는 함수는 setenv, putenv가 있으며 환경 변수를 해제하려면 unsetenv함수를 사용할 수 있습니다. 환경 변수의 어떤 값을 읽어올 경우에는 getenv 함수를 이용하면 됩니다.

4.1 setenv, unsetenv

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);

setenv 함수는 name에 해당하는 value를 갖는 환경 변수를 설정해줍니다. overwrite1이라면 기존에 환경 변수값이 있을 때 현재 값으로 덮어쓰고, 0이라면 덮어쓰지 않습니다.

unsetenv 함수는 name에 해당하는 환경 변수를 해제합니다.

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

4.2 putenv

#include <stdlib.h>
int putenv(char *string);

putenv이름=형식으로 넘겨줌으로써 환경 변수를 설정할 수 있습니다. 이때 무조건 덮어쓰기가 되니 주의하세요.

성공시 0, 실패시 0이 아닌 값을 반환합니다.

4.3 getenv

#include <stdlib.h>
char *getenv(const char *name);

getenv함수는 name에 해당하는 환경변수를 읽어옵니다. 그래서 그 값을 반환하죠. 만약에 해당하는 환경 변수가 없다면 NULL을 반환합니다.

4.4 환경 변수 함수 이용의 예

네 함수(setenv, putenv, getenv unsetenv)를 다 사용하는 예제를 한번 볼까요.

//setenv.c
#include <stdio.h>
#include <stdlib.h>

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

        char *name = NULL;
        char *value = NULL;
        char name_val_pair[128]={0,};
        if(argc != 3){
                printf("Usage %s NAME VALUE\n", argv[0]);
                return 1;
        }

        name = argv[1];
        value = argv[2];

        //overwrite 허용
        //setenv(name, value, 1);
        //overwrite 불가
        //setenv(name, value, 0);
        sprintf(name_val_pair, "%s=%s",name,value);
        putenv(name_val_pair);

        printf("%s=%s\n",name,getenv(name));

        unsetenv(name);
}

putenv를 이용해서 환경 변수를 설정해보았습니다. 만약 여러분이  setenv함수를 이용해서 덮어쓸지 말지를 결정할 수도 있습니다. 여기서는 putenv를 통해서 덮어쓰기가 가능한 환경 변수 설정을 하는 것으로 합시다. 다음은 환경 변수 USER를 바꾸는 실행화면입니다. 처음에는 ubuntu로 설정되어있고, 프로그램을 실행하니까 kali로 바뀌었습니다.

그런데 프로그램이 끝난 이후 USER를 확인해보니 ubuntu로 돌아왔네요. 왜 그렇죠!? 용(3 환경변수의 특성)을 잊지 않으셨죠?

# echo $USER
ubuntu
# ./a.out USER kali
USER=kali
# echo $USER
ubuntu

만약 USER가 바껴지는 것을 확인하고 싶다면 system함수로 다른 shell을 실행시켜셔 확인해보세요. system 함수는 다른 명령어를 실행하는 함수로 이해하시면 됩니다.

//setenv.c
//… 생략 … //

int main(int argc, char *argv[]){
	//… 생략 … //
	putenv(name_val_pair);

        printf("%s=%s\n",name,getenv(name));
        system("/bin/sh");

        unsetenv(name);
}
# ./a.out USER kali
USER=kali
# printenv | grep USER    sh쉘로 전환
USER=kali

 

로그인시 환경변수 자동 셋팅(.bashrc), 전체 사용자에 대한 환경 변수 설정(/etc/profile)에 대한 설정 방법은 아주 훌륭하신 블로거님들이 정리를 잘 해놓았으니, 여기서는 설명하지 않겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

한계

리눅스에서 프로그램을 다른 리눅스에서 실행할 수 있습니다. 혹은 다른 유닉스 계열(BSD나 솔라리스, MAC OS X 등)의 시스템에서도 동작이 될 수가 있지요. 그런데 이렇게 시스템마다 지원하는 한계라는 것이 다 제 각각이거든요. 예를 들면 어떤 시스템에서는 한 프로세스 당 열 수 있는 파일 디스크립터의 갯수가 256개를 지원하는데, 다른 시스템에서는 1024개까지 지원할 수 있는 등 시스템마다 지원할 수 있는 한계가 존재합니다. 그렇다면 어떤 시스템에서 사용자가 생성할 수 있는 자식 프로세스의 수라던가, 최대 파일 경로의 이름이라던가,  로그인이 가능한 이름의 최대 길이라던가를 알아내면 그 프로그램이 효율적으로 동작할 수 있을 것 같은데요. 과연 어떻게 알아낼 수 있을까요?

한계는 세가지로 구분할 수가 있습니다. 

  • 컴파일 시점에서 한계(헤더 파일에 명시) : 이러한 한계들은 limits.h에 명시되어 있습니다. 시스템마다 불변하는 한계를 의미하게 됩니다. 예를 들어 POSIX를 준수하는 시스템에서는 int 자료형에서 지원가능한 값은 2,147,483,647이 적어도 만족이 되어야합니다. INT_MAX 상수로 확인이 가능합니다. 여기서는 설명하지 않겠습니다. 
  • 파일이나 디렉토리와 연관되지 않은 실행 시점의 한계(sysconf 함수) : 파일과 연관이 없는 시스템에서 실제 지원할 수가 있는 한계를 확인하려면 sysconf함수를 이용해서 확인할 수 있습니다. 함수의 원형을 볼까요?
#include <unistd.h>

long sysconf(int name);

   sysconf의 인자인 name을 전달하게 되면 그에 따른 값이 나옵니다. name은 앞에 _SC_로 시작하는 매크로 상수입니다. SC는 SysConf의 약자입니다. 아래의 소개한 매크로보다 더 다양한 name이 있습니다. 여기서는 요만큼만 설명합니다.

매크로 상수 설명
_SC_ARG_MAX exec 함수의 인수 길이 최대값
_SC_CHILD_MAX uid 당 동시에 실행할 수 있는 프로세스의 최대값
_SC_HOST_NAME_MAX hostname의 최대 값, gethostname으로 구할 수 있습니다
_SC_LOGIN_NAMX_MAX 로그인 이름의 최대 길이 
_SC_OPEN_MAX 프로세스가 열 수 있는 파일의 최대 갯수
_SC_PAGE_SIZE
_SC_PAGESIZE
페이지의 크기
_SC_STREAM_MAX 프로세스가 열 수 있는 파일 스트림의 최대 갯수
_SC_TTY_NAME_MAX 터미널 디바이스 이름의 최대 길이
_SC_TZNAME_MAX 타임 존 이름의 최대 길이 
_SC_LINE_MAX 유틸리티 프로그램에서 입력 줄로 받을 수 있는 최대 길이(stdin으로든 file으로 든)
_SC_SIGQUEUE_MAX 한 프로세스에 신호 큐에 담을 수 있는 신호의 최대 갯수
_SC_SEM_VALUE_MAX 세마포어의 최대값
_SC_SEM_NSEMS_MAX 한 프로세스가 동시에 사용할 수 있는 세마포어의 최대 개수
_SC_CLK_TCK 1초 클록 틱 개수

 

  • 파일이나 디렉토리와 연관된 실행 시점의 한계(pathconf 함수 혹은 fpathconf 함수) : 파일과 관련된 한계를 알아낼 때는 pathconffpathconf 함수를 사용할 수 있습니다. 예를 들어 어떤 터미널에 대한 한계를 알고 싶다면 /dev/ 하위의 터미널 파일을 입력으로 주어 확인할 수 있습니다. 
#include <unistd.h>

long fpathconf(int fd, int name);
long pathconf(const char *path, int name);

 이 둘의 동작은 같습니다. 단지 파일을 알려주는 첫 인자를 파일 디스크립터로 전달하느냐(fd), 파일의 경로와 이름을 사용하여 전달하느냐(path)에 따른 것만 다르죠. name은 확인하려는 한계의 이름을 넣어주면 됩니다. 앞에 _PC_로 시작합니다. Path Config의 약자겠죠? 

매크로 상수 설명
_PC_LINK_MAX 파일에 최대 링크 갯수를 의미하며, 만약 파일이라면 파일에 대한 최대 링크 갯수를 가져오고, 디렉토리를 지정하면 디렉토리의 링크 최대 갯수를 가져옵니다.
_PC_MAX_CANON 터미널의 서식화된 입력줄의 최대 길이로 파일을 반드시 터미널 파일을 지정해야합니다.
_PC_MAX_INPUT 터미널의 입력줄의 최대 길이로 반드시 터미널 파일을 지정해야합니다.
_PC_NAME_MAX 지정한 디렉토리에서 파일 입력의 최대 길이로 인자를 디렉토리로 주어야합니다.
_PC_PIPE_BUF 하나의 파이프에 원자적으로 쓸 수 있는 최대 바이트 수로 pipe나 fifo 타입의 파일, 혹은 디렉토리를 주어야합니다. 디렉토리를 주었을 때는 디렉토리 안에 생성된 임의의 fifo에 대한 한계를 가져옵니다.
_PC_PATH_MAX path나 fd가 현재 작업 디렉토리일 경우 상대 경로의 최대길이

 

위 세 함수(sysconf, pathconf, fpathconf)는 실패일 경우 -1이 반환되지만 1) 실제 지원하지 않는 name이라서 실패한 경우2)확정할 수 없는 한계에 의한 실패가 있습니다. 지원하지 않는 실패의 경우 errno가 EINVAL로 설정되구요. 확정할 수 없는 한계에 의한 실패는 errno가 변하지 않습니다.

한계를 가져오는 소스 코드

//print_conf.c
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

static void print_sysconf(char *str, int name){
        long val;
        errno = 0;
        printf("%s ", str);

        if((val = sysconf(name)) < 0){ 
                if(errno != 0){
                        if(errno == EINVAL)
                                printf(" (not supported)\n");
                        else printf("syconf error\n");
                }else
                        printf(" (no limit)\n");

        }else 
                printf(" %ld\n", val);
}

static void print_pathconf(char *str, char *path, int name){
        long val;
        printf("%s ", str);
        errno = 0;
        if((val = pathconf(path,name)) < 0){ 
                if(errno != 0){
                        if(errno == EINVAL)
                                printf(" (not supported)\n");
                        else printf("pathcon error(%s)\n", path);
                }else
                        printf(" (no limit)\n");
        }else
                printf(" %ld\n", val);

}
int main(int argc, char *argv[]){
        if(argc != 2){
                printf("Usage : %s <filename> \n", argv[0]);
                return 1;
        }


        printf("======== sysconf ==========\n");
#ifdef _SC_ARG_MAX
        print_sysconf("ARG_MAX :", _SC_ARG_MAX);
#endif
#ifdef _SC_CHILD_MAX
        print_sysconf("CHILD_MAX : ", _SC_CHILD_MAX);
#endif
#ifdef _SC_HOST_NAME_MAX
        print_sysconf("HOST_NAME_MAX : ", _SC_HOST_NAME_MAX);
#endif
#ifdef _SC_LOGIN_NAMX_MAX
        print_sysconf("LOGIN_NAMX_MAX : ", _SC_LOGIN_NAMX_MAX);
#endif
#ifdef _SC_OPEN_MAX
        print_sysconf("OPEN_MAX : ", _SC_OPEN_MAX);
#endif
#ifdef _SC_PAGESIZE
        print_sysconf("PAGESIZE : ", _SC_PAGESIZE);
#endif
#ifdef _SC_STREAM_MAX
        print_sysconf("STREAM_MAX : ", _SC_STREAM_MAX);
#endif
#ifdef _SC_TTY_NAME_MAX
        print_sysconf("TTY_NAME_MAX : ", _SC_TTY_NAME_MAX);
#endif
#ifdef _SC_TZNAME_MAX
        print_sysconf("TZNAME_MAX : ", _SC_TZNAME_MAX);
#endif
#ifdef _SC_LINE_MAX
        print_sysconf("LINE_MAX : ", _SC_LINE_MAX);
#endif
#ifdef _SC_SIGQUEUE_MAX
        print_sysconf("SIGQUEUE_MAX : ", _SC_SIGQUEUE_MAX);
#endif
#ifdef _SC_SEM_VALUE_MAX
        print_sysconf("SEM_VALUE_MAX : ", _SC_SEM_VALUE_MAX);
#endif
#ifdef _SC_SEM_NSEMS_MAX
        print_sysconf("SEM_NSEMS_MAX : ", _SC_SEM_NSEMS_MAX);
#endif
#ifdef _SC_CLK_TCK
        print_sysconf("CLK_TCK : ", _SC_CLK_TCK);
#endif

        printf("======== pathconf ==========\n");
#ifdef _PC_LINK_MAX
        print_pathconf("LINK_MAX : ", argv[1], _PC_LINK_MAX);
#endif
#ifdef _PC_MAX_CANON
        print_pathconf("MAX_CANON : ", argv[1], _PC_MAX_CANON);
#endif
#ifdef _PC_MAX_INPUT
        print_pathconf("MAX_INPUT : ", argv[1], _PC_MAX_INPUT);
#endif
#ifdef _PC_NAME_MAX
        print_pathconf("NAME_MAX : ", argv[1], _PC_NAME_MAX);
#endif
#ifdef _PC_PIPE_BUF
        print_pathconf("PIPE_BUF : ", argv[1], _PC_PIPE_BUF);
#endif
#ifdef _PC_PATH_MAX
        print_pathconf("PATH_MAX : ", argv[1], _PC_PATH_MAX);
#endif

}

 

위의 소스코드는 sysconf와 pathconf 함수를 사용해서 한계를 출력해주는 프로그램입니다. 다른 시스템에서는 정의되지 않은 name이 존재할 수 있으므로 #ifdef로 정의되어있는지 파악하여 사용합니다. 

실패할 경우에는 errno이 EINVAL이면 지원하지 않는 한계 이름입니다. 그런데 errno가 0으로 변하지 않았다면 불확정인 한계로 볼 수 있습니다. 

errno = 0;
//...
if((val = sysconf(name)) < 0){ 
    if(errno != 0){
            if(errno == EINVAL)
                    printf(" (not supported)\n");
            else printf("syconf error\n");
    }else
            printf(" (no limit)\n");

 

아래는 실행 결과입니다.

# ./a.out /etc
======== sysconf ==========
ARG_MAX :  2097152
CHILD_MAX :   15044
HOST_NAME_MAX :   64
OPEN_MAX :   1024
PAGESIZE :   4096
STREAM_MAX :   16
TTY_NAME_MAX :   32
TZNAME_MAX :   (no limit)
LINE_MAX :   2048
SIGQUEUE_MAX :   15044
SEM_VALUE_MAX :   2147483647
SEM_NSEMS_MAX :   (no limit)
CLK_TCK :   100
======== pathconf ==========
LINK_MAX :   65000
MAX_CANON :   255
MAX_INPUT :   255
NAME_MAX :   255
PIPE_BUF :   4096
PATH_MAX :   4096

 

반응형
블로그 이미지

REAKWON

와나진짜

,

버퍼

C 표준입출력 라이브러리에서는 내부적으로 버퍼를 도입하여 입출력을 효율적으로 처리합니다. printf나 scanf 등의 라이브러리 함수는 결국 입력과 출력을 read, write를 통해서 이루어집니다. 버퍼라는 공간을 두는 이유는 내부적으로 write, read를 적시에 최소한으로만 호출하기 위한 것이 목적입니다. 왜 그런 뻘짓을 하느냐구요? CPU를 많이 사용하지 않기 위해서입니다. 한 글자 씩 계속 입력을 받거나 출력을 하면 그만큼 read, write 콜이 잦아지는데 이러면 CPU에 부담이가 성능에 안좋을 영향을 끼치게 되는 겁니다. 버퍼를 사용하는 것은 라이브러리 함수인면에서 응용 프로그램에서는 신경쓰지 않아도 되지만, 버퍼의 처리 방식을 모르게 되면 낭패를 봅니다.

어떻게 버퍼를 이용하는 것을 버퍼링이라고 하고  따라서 세가지 버퍼링 방식이 있습니다. 

전체 버퍼링(Full buffering)

이러한 버퍼링은 내부 버퍼에 데이터가 꽉 차게 되면 그제서야 입출력이 되는 방식입니다. 그러니까 버퍼가 전부 차기 전에는 이 데이터를 가지고만 있고 입출력은 하지 않는 것이죠. 위에서 얘기했듯이 이러한 버퍼링의 목적은 read, write를 최소한으로 사용하기 위함입니다. 그래서 버퍼가 전부 찰 때까지 기다리고 있죠. 이 때 "버퍼의 크기가 크면 무조건 좋은 거 아닌가?" 라는 물음을 던질 수 있는데, 정도라는 것이 있듯 최적의 버퍼 크기가 정해져있습니다. 이것을 표준 입출력 라이브러리가 정해줍니다. 우리는 개-꿀만 빨면 됩니다.

보통 파일을 디스크로부터 읽을 때의 버퍼링 방식입니다. 

아래와 같은 경우가 전체 버퍼링의 예를 보여줍니다. 붉은 사각형은 비어있는 데이터를 의미하며 파란 사각형은 채워진 데이터를 의미합니다. 현재는 버퍼에 2바이트의 데이터가 모자라서 파일에 기록하지 않고 있습니다. 이때 2바이트가 채워지고 있는 모습입니다. 

전체 버퍼링1

이 때 완전히 버퍼가 채워지면 그제서야 데이터를 한꺼번에 파일로 전송하게 됩니다. 

 

줄 단위 버퍼링(Line buffring)

scanf나 fgets, fgetc 등의 표준 입력 함수나 printf, fputs, putc 등의 함수를 이용한 표준 출력을 사용할 때 이러한 줄 단위 버퍼링이 적용됩니다. 줄 단위 버퍼링은 새 줄 문자('\n')가 나올 때 까지 입력이나 출력을 하는 것입니다. 또한 버퍼가 차게 되면 입출력을 진행합니다. 이 때 버퍼의 크기는 보통 전체 버퍼링의 버퍼 크기보다 작습니다.

아래와 같은 경우가 줄 단위 버퍼링을 보여줍니다. 아직 데이터가 전부 채워지지 않았으며 이 때 개행 문자인 '\n'이 입력이 되고 있는 상황입니다. 

개행 문자를 만나면 버퍼가 채워져있지 않음에도 입출력을 진행하게 됩니다. 

 

비 버퍼링(Unbuffered)

버퍼링은 하지 않는 방식입니다. 왜요? 급하기 때문입니다. 여러분도 급똥이면 장사없듯이 프로그램도 급하면 장사없습니다. 언제가 급할까요? 에러를 출력할때가 그런 상황입니다. 지체없이 에러를 해결해야할 상황이 생기기 때문이지요. 

버퍼링 정보 가져오기

그렇다면 보통의 표준 입력, 표준 출력, 표준 에러나 파일에 대한 스트림은 어떤 버퍼링 방식을 갖고 버퍼 크기는 어떻게 결정이 될까요? 아래의 코드는 상황에 따른 입,출력 버퍼에 대한 정보를 표시해주는 코드입니다.

//buffer_info.c

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

#ifdef __GLIBC__
#define _IO_UNBUFFERED 0x0002
#define _IO_LINE_BUF 0x0200
#endif

int main(int argc, char *argv[]){
        FILE *fp;

        char buf[32] = {0, };

        if(argc =! 2){
                printf("Usage : %s stdin | stdout | stderr | file_name\n",
                                argv[0]);
                return 1;
        }


        if(!strcmp(argv[1], "stdin")){
                fp = stdin;
                fgets(buf, sizeof(buf), fp);
        } else if(!strcmp(argv[1], "stdout")){
                fp = stdout;
                printf("stdout\n");
        } else if(!strcmp(argv[1], "stderr")){
                fp = stderr;
                fprintf(fp, "stderr\n");
        } else {
                fp = fopen(argv[1], "r");

                if(fp == NULL){
                        printf("fopen error\n");
                        return 1;
                }

                while(fgets(buf, sizeof(buf), fp) != NULL);
        }

        if(fp->_flags & _IO_UNBUFFERED)
                printf("비버퍼링\n");
        else if(fp->_flags & _IO_LINE_BUF)
                printf("줄단위 버퍼링\n");
        else
                printf("전체 버퍼링\n");

        printf(" 버퍼 사이즈 : %ld\n", fp->_IO_buf_end - fp->_IO_buf_base);

        fclose(fp);
}

 

# ./a.out stdin
hello
줄단위 버퍼링
 버퍼 사이즈 : 1024
# ./a.out stdout
stdout
줄단위 버퍼링
 버퍼 사이즈 : 1024
# ./a.out stderr
stderr
비버퍼링
 버퍼 사이즈 : 1
# ./a.out /etc/group
전체 버퍼링
 버퍼 사이즈 : 4096

 

단순히 stdin, stdout, stderr에 대해서 fgets나 printf를 한번 호출하지 않고서 fp->_flags를 들여다보면 다른 결과가 나올 수 있습니다. 예를 들면 아래와 같이 fgets를 주석 처리하고 실행해보시면 다른 결과를 보실 수 있을 겁니다. 

    if(!strcmp(argv[1], "stdin")){
            fp = stdin;
            //fgets(buf, sizeof(buf), fp);

아래의 결과가 위처럼 fgets를 주석처리한 예인데, 결과가 다르죠?

# ./a.out stdin
전체 버퍼링
 버퍼 사이즈 : 0

이러한 결과를 통해서 알 수 있는 것은 스트림을 열었다고 해서 그 버퍼링이 설정된다는 것이 아니라, read, write를 하는 함수들이 버퍼링을 결정해준다는 사실입니다. 

 

버퍼링 설정 

1. setbuf

#include <stdio.h>

void setbuf(FILE *stream, char *buf);

setbuf 함수를 이용해서 버퍼링 방식을 설정할 수 있습니다. 대신 시스템이 정해준 버퍼인 BUFSIZ로만 사용이 가능합니다. 반대로 버퍼링을 끌 수도 있습니다.

버퍼를 설정하려면 buf[BUFSIZ]의 버퍼를 *buf인자에 전달해야하고, 버퍼를 끄려면 NULL을 전달하면 됩니다.

아래의 예를 한번 볼까요?

//setbuf.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

//버퍼링을 키거나 끄는 쓰레드 
void* buf_control(void *arg){
        char buf[BUFSIZ] = {0,};
        int on;
        while(1){
                scanf("%d", &on);
                switch(on){
                        case 0: //OFF
                                setbuf(stdout, NULL);
                                break;
                        case 1: //ON
                                setbuf(stdout, buf);
                                break;
                }
        }

}

//1초마다 "A"를 계속 찍는 쓰레드
void* print_line(void *arg){
        while(1){
                printf("A");
                sleep(1);
        }
}
int main(int argc, char *argv[]){
        pthread_t tid1, tid2;

        printf("[0] 버퍼 동작 X\t [1] 버퍼 동작 O\n");

        pthread_create(&tid1, NULL, buf_control, NULL);
        sleep(1);
        pthread_create(&tid2, NULL, print_line, NULL);

        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
}
# ./a.out 
[0] 버퍼 동작 X  [1] 버퍼 동작 O
0
AAAAAAAAAAAAAAA1
0
AAAAAAAAAAAA1A
0
AAAAA1
0
AAA1
0
AAAAA^C

 

pthread의 개념을 몰라도 좋습니다. 단순히 buf_control이라는 함수, print_line이라는 함수가 동시에 수행되는 것만 아시면 됩니다. 

buf_control이라는 함수에서는 버퍼링을 끄거나 키거나 할 수 있는데, 1은 버퍼링을 키는 동작, 0은 버퍼링을 끄는 동작이라는 것을 볼 수 있을 겁니다. 

print_line함수는 한 글자씩 1초마다 printf를 통해서 출력을 해주는 함수입니다. 단 줄바꿈(\n)은 하지 않죠. printf는 디폴트 동작으로는 줄단위 버퍼링 방식을 사용하는 것을 위에서 확인했었죠?? 그래서 버퍼링을 설정하게 되면 줄바꿈이 나오지 않거나 버퍼 크기인 1024바이트가 채워지지 않으면 출력을 하지 않습니다.

2. setvbuf

#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

세 가지의 버퍼링 방식을 설정할 수 있습니다. stream에 대해서 size만큼의 buf를 버퍼링합니다. 이때 mode는 비버퍼링(unbuffered), 줄 단위 버퍼링(line buffering), 전체 버퍼링(full buffering)을 설정할 때 쓰입니다. mode에 대한 설명은 아래를 참고하세요. 

mode 설명 
_IONBF 비버퍼링 모드 
_IOLBF 줄 단위 버퍼링 모드
_IOFBF 전체 버퍼링(블록 버퍼링) 모드

setvbuf는 성공시 0, 실패시 0이 아닌 값을 설정하여 돌려줍니다.

확실히 setbuf 함수보다는 보다 세세한 설정이 가능하죠? 그렇다면 setvbuf함수를 통해서 버퍼링을 설정하는 예를 볼까요? 아래는 stdout을 줄 단위 버퍼링이 아닌 4바이트의 전체 버퍼링으로 바꾼 하나의 예입니다.

//setvbuf.c
#include <stdio.h>
#include <unistd.h>

#ifdef __GLIBC__
#define _IO_UNBUFFERED 0x0002
#define _IO_LINE_BUF 0x0200
#endif

#define BUF_SIZE 4

int main(){

        char buf[BUF_SIZE] = {0,};
        FILE *fp = stdout;

        if(setvbuf(fp, buf, _IOFBF, BUF_SIZE) != 0){
                printf("setvbuf _IOLBF error \n");
                return 1;
        }

        if(fp->_flags & _IO_UNBUFFERED) printf("비버퍼링\n");
        else if(fp->_flags & _IO_LINE_BUF) printf("줄단위 버퍼링\n");
        else printf("전체 버퍼링\n");

        while(1){
                printf("A");
                sleep(1);
        }

}

실행해보면 줄 단위 버퍼링이 아닌 전체 버퍼링으로 설정된 것을 볼 수 있습니다. 그리고 4초마다 버퍼가 꽉 채워지기 때문에 출력이 되는 동작을 확인할 수 있네요.

# ./a.out 
전체 버퍼링
AAAAAAAAAAA^C
반응형
블로그 이미지

REAKWON

와나진짜

,