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

와나진짜

,

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

와나진짜

,

저장된 사용자 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

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

FIFO(Named Pipe)

pipe(fd[2])를 호출해서 만들어진 파이프는 이름이 붙어있지 않습니다. 그렇기 때문에 부모 프로세스나 자식 프로세스와 같이 연관된 프로세스에서 사용할 수 있습니다. 물론 부모 프로세스가 파이프를 생성하고, 자식 프로세스 2개를 생성한 후에 그 자식 프로세스들이 부모 프로세스가 생성한 파이프를 쓰는 것도 가능합니다. 한계점은 전혀 연관없는 프로세스는 파이프를 사용하여 입출력할 수 없다는 점인데요. 파이프의 이러한 한계를 개선한 것이 FIFO입니다.

FIFO는 다른 말로 이름있는 파이프, 명명된 파이프(named pipe)라고 합니다. 파이프라는 특징이 결국 먼저 들어간 것이 먼저 나오는 구조인 선입선출(Fitst In First Out)의 특징, 그러니까 먼저 먹은걸 먼저 싼다는 개념을 갖기 때문이죠. 이름이 있기 때문에 연관없는 다른 프로세스가 그 이름을 가진 파이프를 찾아내어 입력이나 출력을 할 수 있게 됩니다. 

mkfifo

이름있는 파이프는 아래의 함수를 호출하여 만들어집니다. fifo는 일종의 파일이기 때문에 open 시스템콜과 매우 유사한 방식으로 만들 수 있다는 점입니다.

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);

 

mkfifo와 mkfifoat의 차이점이라고 한다면 상대경로일 경우 dirfd에서 시작하느냐 아니면 현재 위치에서 시작하느냐의 차이입니다. mode : mkfifo 생성시에  권한을 부여합니다. open에서의 mode와 비슷합니다.

mkfifo를 통해서 파이프를 생성하면 왠 파일이 생성되는 것을 확인할 수 있습니다. fifo 역시 파일의 한 종류이기 때문에 unlink(삭제)가 가능합니다. 

예제

fifo는 pipe이기 때문에 한쪽에서는 읽기만, 한쪽에서는 쓰기만 할 수 있습니다. 만약 클라이언트의 메시지를 다시 되돌려주는 에코 서버를 만들려면 클라이언트로부터 읽기, 클라이언트로 쓰기를 다 해야하는데, 이럴경우는 어떻게할까요? 파이프 2개를 사용하면 됩니다. 그래서 아래와 같이 구현이 가능하죠. to-server.fifo와 to-client.fifo라는 명명된 파이프 2개를 야무지게 사용하는 것을 확인할 수 있죠?

 

fifo를 이용해서 일종의 서버와 클라이언트 프로그램을 만들어보도록 합시다.

//fifo_server.c
//
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

#define MAX_BUF 128

#define TOSERVER "to-server.fifo"
#define TOCLIENT "to-client.fifo"

void norm_exit(){
        unlink(TOSERVER);
        unlink(TOCLIENT);
        exit(0);
}
void sig_int(int signo){
        norm_exit();
}
int main(){
        int readfd, writefd, n; 
        char buf[MAX_BUF]={0,};

        if(signal(SIGINT, sig_int) == SIG_ERR){
                perror("signal error ");
                exit(1);
        }

        if(mkfifo(TOSERVER, 0666) < 0){
                perror("mkfifo for reading error ");
                exit(1);
        }
        if(mkfifo(TOCLIENT, 0666) < 0){
                perror("mkfifo for writing error ");
                exit(1);
        }

        //다른쪽에서 fifo를 열때까지 대기한다.
        readfd = open(TOSERVER, O_RDONLY);
        writefd = open(TOCLIENT, O_WRONLY);


        if(readfd < 0 || writefd < 0){
                perror("open error ");
                exit(1);
        }

        printf("server start\n");

        while(1){

                //readfd를 통해서 입력받는다
                if((n = read(readfd, buf, MAX_BUF)) < 0){
                        perror("read error ");
                        exit(1);
                }

                //한쪽에서 fifo를 닫으면 파일끝을 만나게 된다. 
                if(n == 0) {
                        printf("file end\n");
                        norm_exit();
                }

                printf("[read message ] %s\n", buf);

                //읽은 메시지를 fifo를 통해 전달한다.
                if((n = write(writefd, buf, n)) < 0){
                        perror("write error ");
                        exit(1);
                }
        }

}

 

위는 서버의 역할을 하는 프로그램입니다. 이 서버 단독으로 실행시에 아마 멈춰있을 겁니다. 

fifo의 기본동작은 파일을 쓰기 전용 - 읽기 전용으로 열려야지 그 다음으로 진행한다는 것입니다. 그래서 writefd가 다른 프로세스에서 O_RDONLY가 될때 다음 라인으로 넘어가고 다시 readfd가 O_WRONLY로 다른 프로세스에 의해서 열려야 다음의 실행으로 넘어갈 수 있다는 뜻입니다.

//다른쪽에서 fifo를 열때까지 대기한다.
writefd = open(TOCLIENT, O_WRONLY);
readfd = open(TOSERVER, O_RDONLY);

일단 열고보겠다면 open시에 O_NONBLOCK을 지정해야합니다. 

다음은 client 역할을 하는 소스코드입니다.  

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

#define MAX_BUF 128

#define TOSERVER "to-server.fifo"
#define TOCLIENT "to-client.fifo"

int main(){
        int readfd, writefd, n; 
        char buf[MAX_BUF]={0,};

        writefd = open(TOSERVER,O_WRONLY);
        readfd = open(TOCLIENT, O_RDONLY);

        if(readfd < 0 || writefd < 0){
                perror("open error ");
                exit(1);
        }

        while(1){

                printf("message :");
                fgets(buf, MAX_BUF, stdin);

                n = strlen(buf) + 1;
                if((n = write(writefd, buf, n)) < 0){
                        perror("write error ");
                        exit(1);
                }

                if((n = read(readfd, buf, MAX_BUF)) < 0){
                        perror("read error ");
                        exit(1);
                }

                printf("[read message ] %s\n", buf);

        }
}

 

컴파일은 아래와 같이 해줍시다.

 

# gcc fifo_server.c -o server
# gcc fifo_client.c -o client

 

이제 실행하면서 어떤 현상이 관찰되는지 확인해볼까요? 클라이언트 실행을 위해 터미널을 2개 사용합시다.

[1]

우선 서버쪽을 보면 그대로 멈춰있는 것을 알 수 있습니다. 이때 client 실행하기 전에 다른 터미널에서 파일 목록을 보면 fifo 파일 두개가 생성되어있음을 확인할 수 있을 건데, 이는 mkfifo를 호출하여 만든 결과입니다.

./server ./client
#./server

# ls -l
total 36
-rwxr-xr-x 1 root root  9264 Jul 13 04:36 client
-rw-r--r-- 1 root root   770 Jul 13 04:00 fifo_client.c
-rw-r--r-- 1 root root  1293 Jul 13 04:35 fifo_server.c
-rwxr-xr-x 1 root root 13432 Jul 13 04:36 server
prw-r--r-- 1 root root     0 Jul 13 04:36 to-client.fifo
prw-r--r-- 1 root root     0 Jul 13 04:36 to-server.fifo

 

이제 클라이언트를 실행해볼게요.

[2]

./server ./client
# ./server 
server start
[read message ] hello fifo server

[read message ] yo shake it!! just shake it !! 

[read message ] good bye
file end
# ./client 
message :hello fifo server
[read message ] hello fifo server

message :yo shake it!! just shake it !! 
[read message ] yo shake it!! just shake it !! 

message :good bye
[read message ] good bye

message :^C

 

서버로부터 에코가 잘되는 것을 확인 할 수가 있습니다. 

마지막 하나의 클라이언트 쪽에서 Ctrl+C를 통해서 종료를 했는데, 이렇게 종료하면 읽는 쪽 server는 read시에 파일의 끝인 0을 반환받게 됩니다. 하나의 클라이언트라서 티가 안나지만 2개 이상 클라이언트가 이런 식의 종료를 하게 되면 볼 수 있습니다.

//한쪽에서 fifo를 닫으면 파일끝을 만나게 된다. 
if(n == 0) {
        printf("file end\n");
        norm_exit();
}

그럴 경우 서버도 종료하게 하였고, 서버쪽에서도 역시 SIGINT로 종료할 수 있게 하였습니다.  종료시에는 fifo 파일을 삭제해주는 코드를 추가해서 이 다음에 서버가 실행이 될 때 이미 파일이 존재한다는 오류를 방지하도록 합시다. 

void norm_exit(){
        unlink(TOSERVER);
        unlink(TOCLIENT);
        exit(0);
}

 

fifo를 통해서 단순히 하나의 클라이언트 뿐만 아니라 여러 클라이언트들도 접속이 가능합니다. 

./server
# ./server 
server start
[read message ]  hello ! i'm first client!

[read message ] hello i'm second client

[read message ] good bye~~

file end
./client ./client
# ./client 
message : hello ! i'm first client!
[read message ]  hello ! i'm first client!

message :good bye~~
[read message ] good bye~~

message :^C
# ./client 
message :hello i'm second client
[read message ] hello i'm second client

message :^C

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

사용자 ID(Real User ID, Effective User ID,  Saved User ID) 그룹 ID(Real Group ID, Effective Group ID, Saved Group ID)

리눅스에서 사용자는 각 User ID라는 값을 갖습니다. 그리고 그 사용자는 어느 그룹의 일원이 되죠. 그래서 Group ID도 같이 갖습니다. 이렇게 id를 부여하는 이유는 각각 사람들마다 다른 권한을 갖고 있기 때문입니다. 각 사용자들은 자신들의 파일을 만들고 그 파일에 대해 권한을 부여합니다. 즉, 이 파일을 어디까지 허용할지 말이죠. 파일을 누구에게 까지 읽고 쓰는 것을 허락하느냐, 실행파일이라면 실행을 누구에게 까지 허락하느냐 이러한 허용 말입니다.

그런데 실행 파일이라면 특수한 권한 하나가 더 생깁니다. 이 파일을 실행할 경우에 갖는 권한, 즉 실행 할 때만 유효한 권한이요. 그게 뭐냐면 실행할 때만 소유주의 권한, 혹은 소유주 그룹의 권한으로 실행되는 권한입니다. 예를 들어서 파일을 읽는 어떤 프로그램이 있다고 칩시다. 그리고 이 프로그램의 소유주는 “기쁨”이라고 할게요. 어떤 사용자 “똘똘이”가 프로그램을 통해서 파일을 읽을 땐 똘똘이의 파일밖에 못 읽습니다. 기쁨이의 파일을 함부로 볼 수가 없죠. 근데 착한 기쁨이는 나의 프로그램을 쓸 때 나의 파일은 읽을 수 있게 특별한 권한을 이 프로그램에 준다면 똘똘이는 기쁨이의 파일도 읽게 될 수 있습니다. 이와 같은 상황을 권한이 확장되었다라고 합니다.

 

전문용어로 정리하면 아래와 같습니다. 그리고 여기서는 uid에만 포커스를 맞춰서 보겠습니다. 왜냐면 gid도 역시 uid와 같은 방식으로 동작되지 때문이죠.

UID 종류 설명
Real UID(User ID)
ruid
실제 사용자 ID를 의미합니다. 여러분들이 로그인할 때 접속하는 그 user의 ID입니다. 이 실제 id에 대한 정보는 /etc/passwd 파일에 기록이 되어있지요. 줄여서 ruid라고 하겠습니다.
Effective UID(User ID)
euid
프로그램 실행 시 갖는 권한을 의미하며 실행 파일의 소유주 권한입니다. 보통은 사용자 ruid와 같습니다. 실행파일의 user 실행 권한에 setuid 권한이 있을 경우에 달라집니다. 줄여서 euid라고 합니다.
Saved UID(User ID)
suid
저장된 사용자의 ID라고 합니다. 프로그램에 권한을 더 쎈 권한을 주어야할 때나 권한을 더 줄여야할 때에 유기적으로 쓰이게 됩니다. 줄여서 suid라고 합니다. suid의 쓰임새에는 조금 나중에(맨 아래에서) 코드로 설명을 드리겠습니다.

 

GID 종류 설명
Real GID(Group ID)
rgid
실제 사용자 그룹의 ID입니다. Ruid와 마찬가지로 로그인할때 부여되는 gid를 의미합니다. 줄여서 rgid라고 합니다.
Effective GID(Group ID)
egid
유효 사용자 그룹의 ID입니다. 역시 보통은 rgid와 같습니다. 실행파일에 setgid비트가 켜져있으면 rgid와 달라질 수 있습니다.
Saved GID(Group ID)
sgid
저장된 그룹의 ID입니다. 줄여서 sgid라고 합니다.

이제 uid와 관련한 함수들을 보면서 어떤 특징을 갖는지 확인해보도록 합시다. 현재 접속한 사용자가 누군지 쉽게 알아보게 하기 위해서 프롬프트 앞에 사용자 계정명을 같이 표시하겠습니다.

1. 사용자 uid 읽기 - getuid, geteuid, getresuid

#include <unistd.h>
uid_t getuid(void);
uid_t geteuid(void);
int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);

getuid는 사용자의 진짜 레알 아이디인 ruid를 가져옵니다.
geteuid는 프로그램의 실행 중 권한 euid를 가져옵니다.
getresuid는 프로그램의 ruid, euid뿐만 아니라 suid까지 가져옵니다. 실패시 -1을 가져옵니다.

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

void pr_resuid(){
        uid_t ruid, euid, suid;
       
        if(getresuid(&ruid, &euid, &suid) < 0){
                printf("getresuid error\n");
                exit(1);
        }

        printf("ruid : %d, euid :%d, suid :%d\n",
                        ruid, euid, suid);

}
int main(){
        pr_resuid();
}

보통의 상황이라면 ruid와 euid, suid는 같게 될 겁니다. 아래와 같이 u+s를 주어 setuid를 설정합니다. 그리고 실행하면 ruid와 euid는 0입니다. 여기까지는 다들 예상 하실 겁니다.

root# ls -l a.out 
-rwxr-xr-x 1 root root 8968 Jun 19 11:27 a.out
root# chmod u+s a.out
root# ./a.out 
ruid : 0, euid :0, suid :0

만약 다른 사용자로 로그인하게 되면 아래와 같이 실제 ruid는 바뀌지 않으나 euid는 바뀌죠. 여기까지 다 아는 내용입니다. 단순히 getresuid를 통해서 세가지 uid(ruid, euid, suid)를 가져오는 예제일 뿐입니다. 이 함수는 3개의 uid인 ruid, euid, suid를 모두 가져올 수 있기 때문에 지금부터 아래의 예제들은 이 함수로 uid들을 출력하도록 하겠습니다. 아, suid는 맨 처음 euid와 같은 점은 눈여겨 보시기 바랍니다.

root# su ubuntu
ubuntu$ ./a.out 
ruid : 1000, euid :0, suid :0

 

2. uid 설정 함수들

setuid, setgid

#include <unistd.h>
int setuid(uid_t uid);
int setgid(uid_t gid);

setuid : 사용자가 루트일 경우 ruid, euid, suid를 전달된 인자인 uid로 변경할 수 있는 함수입니다(ruid = euid = suid = uid). 다만, 일반 조건에 맞는 유저일 경우만 제한적으로 euid만 자신의 uid로 변경됩니다.

자, 여기서 잘 생각해보세요. 아무나 ruid, 혹은 euid 혹은 suid를 바꾸면 될까요? 안될까요? 그러니까 아무나 본인의 학번이나 사번을 바꾼다고 생각해보세요. 있을 수가 없는 일이죠. 결론부터 말씀드리자면 setuid는 프로그램이 루트 권한으로 실행되면 루트사용자일 경우에 ruid, euid, suid 모두 변경합니다. 루트는 킹 중의 킹, 전설의 레전드입니다. 루트가 바꾸면 토 달지말고 그냥 바꾸는 겁니다.

다시 말하면 프로그램의 euid가 루트일때, setuid는 ruid, euid, suid 모두 바꿔버립니다. 즉, “유효 사용자 id가 root이면(현재 프로그램의 euid가 root이면) 모든 id(ruid, euid, suid)를 setuid를 통해서 바꿀 수 있다.” 입니다. 그런데 루트 사용자의 권한이 아닌 프로그램이고 그 안에서 setuid를 통해서 인자인 uid를 바꾸려하면, 사용자의 ruid 혹은 suid가 setuid의 인자로 전달되는 uid와 같을 때 euid만 변경이 됩니다. 그러니까 루트 사용자가 아닌 경우에는 실제 uid, 즉 ruid는 죽었다 깨어나도 변경할 수 없다는 의미입니다. euid만 바꿀 수 있습니다. 자신의 ruid 혹은 suid로만 말이죠.

얘기가 길었죠. 간단히 정리하면, 현재 유효 사용자 ID가 루트인 경우 ruid, euid, suid 가 원하는 uid로 바꿀 수 있습니다. 현재 유효 사용자 ID가 일반 유저인 경우 현재의 실 사용자 id(ruid) 혹은 현재의 저장된 사용자 id(suid)로 euid만 바꿀 수 있습니다.

다음은 setuid의 특징을 알아보는 코드입니다.

 

//setuid.c

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

void pr_resuid(){
        uid_t ruid, euid, suid;
       
        if(getresuid(&ruid, &euid, &suid) < 0){
                printf("getresuid error\n");
                exit(1);
        }

        printf("ruid : %d, euid :%d, suid :%d\n",
                        ruid, euid, suid);

}

int main(int argc, char *argv[]){
        uid_t uid;
        if(argc < 2){
                printf("%s uid(>=0)\n", argv[0]);
                return 0;
        }

        pr_resuid();

        uid = atoi(argv[1]);
        printf("setuid(%d)\n", uid);
        if(setuid(uid) == -1){
                printf("setuid error : %s\n", strerror(errno));
                exit(0);
        }

        pr_resuid();
}

ubuntu의 계정으로 컴파일 합니다. 이때 ruid는 1000번입니다.

ubuntu$ gcc setuids.c 
ubuntu$ chmod u+s a.out
ubuntu$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd)

이 프로그램을 다른 계정으로 실행해보면 어떻게 될까요? euid 1000인 프로그램에서 실제 ruid가 다른 1001로 바꾸려고 할 때는 아래와 같이 에러가 발생합니다. 당연하겠죠. euid도 누군가가 함부로 남의 euid를 쓸 수는 없는 겁니다.

ubuntu$ ./a.out 1000
ruid : 1000, euid :1000, suid :1000
setuid(1000)
ruid : 1000, euid :1000, suid :1000

ubuntu$ ./a.out 1001
ruid : 1000, euid :1000, suid :1000
setuid(1001)
setuid error : Operation not permitted

 

아까 조건에서도 봤듯이 ruid 혹은 suid가 변경하려는 uid와 같아야 euid만 변경이 됩니다. 여기 1001인 유저 hello로 로그인해서 확인해보겠습니다.

ubuntu$ su hello
Password: 
hello$ id
uid=1001(hello) gid=1001(hello) groups=1001(hello) 

hello$ ./a.out 1000    <- ruid:1001, suid: 1000이므로 euid를 1000으로 변경 가능
ruid : 1001, euid :1000, suid :1000
setuid(1000)
ruid : 1001, euid :1000, suid :1000

hello$ ./a.out 1001  <- ruid:1001, suid: 1001이므로 euid를 1001으로 변경 가능
ruid : 1001, euid :1000, suid :1000
setuid(1001)
ruid : 1001, euid :1001, suid :1000

hello$ ./a.out 1       <- ruid:1001, suid: 1000이므로 1로 변경 불가 
ruid : 1001, euid :1000, suid :1000
setuid(1)
setuid error : Operation not permitted

그렇다면 만약 프로그램의 주인이 루트라면 위 상황은 어떻게 될까요? 아래 명령을 통해서 owner를 바꾸고 setuid도 같이 바꿔줍니다.

root# chown root:root a.out     <- 절대무적 루트가 나타나 owner를 자신으로
root# chmod u+s a.out               <- 실행시 root의 권한을 갖는다.

그리고 계정을 root로 바꿔서 실행해보면 ruid, euid, suid를 전부 바꾼다는 것을 알 수 있습니다. 이는 프로그램이 실행될 때 root의 권한으로 실행(초기 euid가 root) 되어지기 때문에 이라는 것을 설명드렸어요. 루트는 절대무적이니까요.

ubuntu$ ./a.out 0
ruid : 1000, euid :0, suid :0.     <- 현재 유효사용자 id(euid)는 root
setuid(0)
ruid : 0, euid :0, suid :0   <- euid가 root이기 때문에 모든id를 바꿀 수 있다.

ubuntu$ ./a.out 1000
ruid : 1000, euid :0, suid :0
setuid(1000)
ruid : 1000, euid :1000, suid :1000

ubuntu$ ./a.out 1001
ruid : 1000, euid :0, suid :0
setuid(1001)
ruid : 1001, euid :1001, suid :1001

ubuntu$ ./a.out 1
ruid : 1000, euid :0, suid :0
setuid(1)
ruid : 1, euid :1, suid :1

 

seteuid, setegid

#include <unistd.h>
int seteuid(uid_t euid);
int setegid(gid_t egid);

현재의 euid를 바꿉니다. 유효사용자가 root라면 euid를 원하는 값으로 바꿀 수 있습니다. 그런데 일반 사용자라면 euid는 현재의 ruid 혹은 현재의 suid로만 바꿀 수 있습니다. 실패시 -1을 반환합니다.

setreuid, setregid

#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);

위 함수들은 ruid, euid를 바꾸는 것 처럼 보이죠? 맞습니다. 그런데 추가로 suid도 바꿉니다. suid는 euid와 같이 바꿉니다. 이전에 보았던 setuid는 일반 유저가 ruid를 바꾸는게 불가능했었습니다. 그런데 이 함수는 가능합니다. 일반유저인 경우 setreuid 함수는 현재 ruid, euid(현재 suid와는 상관없습니다.) 중에서만 변경할 수 있습니다. 변경을 원치 않는 uid인 경우 -1을 넣어주면 됩니다.

ex) setreuid(-1, 1000);

실패시 -1을 반환합니다.

setresuid, setresgid

#include <unistd.h>
int setresuid(uid_t ruid, uid_t euid, uid_t suid);
int setresgid(gid_t rgid, gid_t egid, gid_t sgid);

만약 ruid, euid, suid를 모두 개별적으로 바꾸고 싶다면 이 함수를 사용하면 됩니다. 단, 이 함수도 아무 id나 바꿀 수는 없겠죠? root를 제외한 나머지 유저들은 ruid, euid, suid 각각의 값을 현재 ruid, euid, suid 중의 값으로만 바꿀 수 있습니다. 실패 시 -1을 반환합니다. 만약 현 uid를 그대로 유지하고 싶다면 해당하는 인자에 -1을 넣어주세요.

그럼 여기까지 uid 설정 함수들을 정리합니다.

현재 ruid, euid, suid = ruid`, euid`, suid`

Ø  유효 사용자 ID = Root

함수 ruid euid suid
setuid ruid`, euid`, suid와 상관없이 원하는 uid로 변경
seteuid 변경 불가 원하는 값으로 설정 가능 변경 불가
setreuid ruid`, euid`, suid와 상관없이 원하는 uid로 변경 euid와 같음
setresuid ruid`, euid`, suid와 상관없이 원하는 uid로 변경

 

Ø  유효 사용자 ID = 일반 유저

함수 ruid euid suid
setuid 변경 불가 ruid` 혹은 suid`로 변경 가능 변경 불가
seteuid 변경 불가 ruid` 혹은 suid`로 변경 가능 변경 불가
setreuid ruid`, euid` 중 하나로 변경 가능 ruid`, euid` 중 하나로 변경 가능 euid와 같음
setresuid ruid`, euid`, suid`  중 하나로 변경 가능 ruid`, euid`, suid`  중 하나로 변경 가능 ruid`, euid`, suid`  중 하나로 변경 가능

 

아래 setuid, setreuid, setresuid를 차례차례 알아보는 코드와 위의 표 내용이 맞는 지 확인하는 실행 결과를 보여줍니다.

//setuids_test.c

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

void pr_resuid(){
        uid_t ruid, euid, suid;

        if(getresuid(&ruid,&euid,&suid) < 0){
                printf("getresuid error\n");
                exit(1);
        }

        printf("ruid : %d, euid : %d, suid : %d\n", 
                        ruid, euid, suid);

}
void setuid_test(){
        uid_t uid;
        printf("uid :");
        scanf("%d", &uid);

        if(setuid(uid) < 0){
                printf("setuid %d로 변경 불가 \n", uid);
        }
        pr_resuid();
}
void seteuid_test(){
        uid_t euid;
        printf("euid :");
        scanf("%d", &euid);
        if(seteuid(euid) < 0){
                printf("seteuid %d로 변경 불가 \n", euid);
        }
        pr_resuid();
}
void setreuid_test(){
        uid_t ruid, euid;
        printf("ruid euid :");
        scanf("%d %d", &ruid, &euid);
        if(setreuid(ruid, euid) < 0){
                printf("setreuid %d %d로 변경 불가\n", ruid, euid);
        }
        pr_resuid();
}
void setresuid_test(){
        uid_t ruid, euid, suid;
        printf("ruid euid suid :");
        scanf("%d %d %d", &ruid, &euid, &suid);
        if(setresuid(ruid, euid, suid) < 0){
                printf("setresuid %d %d %d로 변경 불가\n", ruid, euid, suid);
        }
        pr_resuid();
}
int main(){
        printf("현재 uid\n");
        pr_resuid();

        while(1){
                char c;
                printf("(1)setuid-test\n");
                printf("(2)seteuid-test\n");
                printf("(3)setreuid-test\n");
                printf("(4)setresuid-test\n");
                printf("(other)exit\n");

                scanf(" %c", &c);
                switch(c){
                        case '1':
                                setuid_test();
                                break;
                        case '2':
                                seteuid_test();
                                break;
                        case '3':
                                setreuid_test();
                                break;
                        case '4':
                                setresuid_test();
                                break;
                        default:
                                return 0;
                }

        }

}

 

아래의 상황은 ubuntu(1000)의 유효사용자 id를 갖는 위 프로그램을 hello(1001)라는 유저가 실행했을 때의 상황입니다.

hello$ ls -l a.out
-rwsr-xr-x 1 ubuntu ubuntu 13616 Jun 23 08:43 a.out
hello$ ./a.out 
현재 uid
ruid : 1001, euid : 1000, suid : 1000
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
1
uid :1001
ruid : 1001, euid : 1001, suid : 1000   <- euid만 변경됨
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
2
euid :1000
ruid : 1001, euid : 1000, suid : 1000  <- 현재 ruid, suid로만 euid만 변경됨
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
3
ruid euid :1000 1001
ruid : 1000, euid : 1001, suid : 1001   <- euid와 suid는 같은 값이 됨
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
3
ruid euid :1000 1000       <- setresuid(1000, 1000, 1000)과 같다.
ruid : 1000, euid : 1000, suid : 1000
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
2
euid :1001   <- 3개의 uid가 전부 같은 1000이므로 euid를 바꿀 어떤 방법이 없다
seteuid 1001로 변경 불가 
ruid : 1000, euid : 1000, suid : 1000
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
4
ruid euid suid :1000 1000 1001  <- 위와 같은 이유로 setresuid를 통해서도 바꿀 수없음
setresuid 1000 1000 1001로 변경 불가
ruid : 1000, euid : 1000, suid : 1000
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
4
ruid euid suid :1000 1000 1000  <- 이게 무슨 의미가 있을까
ruid : 1000, euid : 1000, suid : 1000
(1)setuid-test
(2)seteuid-test
(3)setreuid-test
(4)setresuid-test
(other)exit
q         <- 분노의 종료

 

실제 사용자 ID(Real UID), 그리고 유효 사용자 ID(Effective UID)는 대충 알겠는데, saved UID는 어떤 경우에 사용이 되는 걸까요? 포스팅이 너무 길어지니 아래의 포스팅에서 설명하도록 하겠습니다.

https://reakwon.tistory.com/234

 

리눅스 - 코드로 이해하는 저장된 사용자 ID(Saved UID)가 있는 이유

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

reakwon.tistory.com

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

메모리 대응 입출력(memory-mapped I/O)

메모리 대응 입출력 기법은 디스크의 파일을 메모리에 한 부분인 버퍼에 대응을 시켜 읽거나 쓰는 동작을 할 수 있는 기법입니다. 그래서 파일 입출력을 위한 read나 write를 사용할 필요가 없습니다. 메모리 대응 입출력을 사용하려면 커널에 파일을 메모리에 대응(mapping)시키겠다고 정보들을 알려줘야합니다. 그렇기 위해서 우리는 mmap함수를 비롯해 몇가지 함수를 사용합니다. 그래서 마지막에는 메모리 대응 입출력을 활용하여 파일을 copy하는 프로그램을 만들어보도록 하겠습니다.

1. mmap 함수

mmap은 아래와 같이 정의가 되어있습니다. 주어지는 정보들은 아래와 같습니다.

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
                 int fd, off_t offset);

 

- 반환값 : 성공시 메모리의 맵핑된 주소를 반환합니다. void* 형으로 자료형에 따라 알맞게 변환할 수 있습니다. 실패시에는 MAP_FAILED를 반환합니다. MAP_FAILED는 (void*) -1을 의미합니다. 

- addr : 메모리 대응 영역의 시작주소를 뜻합니다. 보통 0을 넣어 적절한 주소를 반환시킵니다. 이때 addr은 시스템의 가상 메모리 페이지의 크기 배수로 설정이 되어야한다는 것을 주의하시기 바랍니다.

- length : 얼마만큼의 메모리를 초기화 할 것인지 크리를 정합니다. 이때 offset 이후부터라는 점을 기억하시기 바랍니다. offset은 아래의 설명이 있습니다.

- prot : 메모리를 보호하는 방식을 설정합니다. 아래의 표와 같이 정의가 되어있습니다. 인수의 이름은 꽤나 직관적이니 뭐 굳이 자세한 설명은 필요없을 것 같네요.

prot 인수 설명
PROT_READ 메모리 영역 읽기 가능
PROT_WRITE 메모리 영역 쓰기 가능
PROT_EXEC 메모리 영역 실행 가능
PROT_NONE 메모리 영역 접근 불가

 

- flags : 메모리 대응 영역에 특성을 설정합니다. 여기서는 세가지만 설명하는데 리눅스 혹은 다른 구현에서는 많은 옵션이 존재할 수 있습니다.

flag 이름 설명
MAP_FIXED 반환값이 정확히 전달받은 addr의 주소와 같은데, 이 flag는 이식성을 저하시키므로 사용하지 않는 것을 권합니다. 
MAP_SHARED 영역에 대응된 파일을 수정합니다. 그러므로 메모리의 수정이 일어나면 파일의 수정이 일어납니다. 아래의 MAP_PRIVATE나 MAP_SHARED 중 하나를 설정해야합니다.
MAP_PRIVATE 유추가 되죠? 영역에 대응된 파일을 수정하는 MAP_SHARED와는 달리 메모리 대응 영역만 수정이 일어날뿐 실제 파일에는 수정이 일어나지 않습니다. 이를 비공유 복사본이 생성된다고 합니다. 

 

- fd : 대응시킬 파일 서술자를 뜻합니다. fd는 어디서 얻어오죠? open에서 얻어올 수 있습니다. 네, mmap을 사용하기 전에 file을 열어야합니다. 

 

- offset : 파일의 시작에서 부터 얼마만큼 떨어진 영역을 메모리에 대응시킬것인가를 뜻합니다. 이떄 offset은 가상 메모리 페이지 크기의 정수배여야합니다.

 > 가상 메모리 페이지 크기

그렇다면 가상 메모리 파이지 크기는 어떻게 알 수 있을까요? 아래의 코드로 얻어올 수 있습니다.

page_size = sysconf(_SC_PAGESIZE);

 

2. munmap

메모리 대응 영역은 프로세스가 종료되면 자동으로 해제가 되는데, munmap을 사용하여 직접 해제시킬 수 있습니다.

#include <sys/mman.h>

int munmap(void *addr, size_t length);

 

- 반환값 : 성공시 0, 실패시 -1을 의미하며 errno에 에러 정보가 저장됩니다.

- addr : 해제할 메모리 영역의 주소를 지정합니다.

- length : 얼마만큼의 영역을 해제할 것인지 size를 지정합니다.

 

3. memcpy

#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);

메모리를 copy하는 함수입니다. 단순히 설명하면 src의 내용을 dest로 n만큼을 복사합니다. 성공시 dest의 주소를 반환하여 줍니다.

 

4. msync

MAP_SHARED으로 메모리를 대응시켰을 경우 일정 시간이 지나면 파일로 내용을 동기시키긴 하지만,  msync 함수를 호출하면 변경된 내용이 바로 실제 파일로 적용이 됩니다. 단, MAP_PRIVATE는 파일에 변경사항이 저장되지 않는 다는 점을 기억하세요. 

#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);

addr과 length는 mmap의 내용과 같습니다. flags의 내용은 대충 아래 표와 같습니다.

flag 설명
MY_ASYNC 페이지들이 호출 반환 후에 방출되게 하고 싶으면 이 인수를 사용합니다.
MY_SYNC 쓰기들이 실제로 완료된 후에 호출이 반환되게 하려면 이 flag를 사용합니다.
둘 중 하나는 설정해야합니다. 

 

5. 메모리 대응 입출력을 이용한 파일 복사 프로그램

cp 명령 아시죠? cp src dst 를 하게 되면 src의 내용이 dst라는 파일로 복사가 됩니다. 아래는 이를 메모리 대응 입출력을 통해서 구현해보는 소스코드입니다.

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

int main(int argc, char *argv[]){
        int srcfd, dstfd; //src 파일 서술자, dst 파일 서술자
        void *src, *dst;  //src 메모리 주소, dst 메모리 주소
        size_t copysz; //다음 copy할  메모리 내용 size
        struct stat sbuf;
        off_t fsz = 0; //다음 읽기, 쓰기를 기록할 위치(offset)
        long page_size; //시스템의 PAGE SIZE

        if((srcfd = open(argv[1], O_RDONLY)) < 0) {
                fprintf(stderr, "can't open %s for reading \n",argv[1]);
                exit(1);
        }

        if((dstfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0777)) < 0){
                fprintf(stderr, "can't open %s for writing\n", argv[2]);
                exit(1);
        }


        //file 사이즈 얻기 위한 용도
        if(fstat(srcfd, &sbuf) < 0){
                fprintf(stderr, "fstat error\n");
                exit(1);
        }

        if(ftruncate(dstfd, sbuf.st_size) < 0){
                fprintf(stderr, "ftruncate error\n");
                exit(1);
        }

        page_size = sysconf(_SC_PAGESIZE);
        printf("page_size : %ld\n", page_size);

        while(fsz < sbuf.st_size){

                if((sbuf.st_size - fsz ) > page_size)
                        copysz = page_size;
                else
                        copysz = sbuf.st_size - fsz;

                //src 주소 설정
                if((src = mmap(0, copysz, PROT_READ, MAP_SHARED, srcfd, fsz))
                                == MAP_FAILED){
                        fprintf(stderr, "mmap error for input \n");
                        printf("error : %s\n",strerror(errno));
                        exit(1);
                }

                //dst 주소 설정 , 여기서 MAP_SHARED를 MAP_RPIVATE로 바꾸면? dst파일에 저장되지 않는다.
                if((dst = mmap(0, copysz, PROT_READ|PROT_WRITE, MAP_SHARED, dstfd, fsz)) == MAP_FAILED){
                        fprintf(stderr, "mmap error for output\n");
                        exit(1);
                }

                //src -> dst로 내용 복사
                memcpy(dst, src, copysz);

                //메모리 해제
                munmap(src, copysz);
                munmap(dst, copysz);
                //복사한 내용만큼 다음 메모리 위치를 이동시킬 offset 증가
                fsz += copysz;

        }

        exit(0);
}

 

argv[1]은 복사할 파일 이름, argv[2]는 복사하여 나온 파일 이름입니다. ftruncate 함수로 출력 파일의 크기를 설정해줍니다. 

ftruncate는 아래와 같이 정의되어 있습니다. 간단히 설명하면 파일 서술자 fd의 길이를 length로 잘라버린다는 겁니다. 즉, 크기 지정한다고 보면 됩니다. 

#include <unistd.h>
#include <sys/types.h>

int ftruncate(int fd, off_t length);

 

이후 sysconf로 현재 나의 컴퓨터의 PAGE SIZE를 가져옵니다. 아까 말했듯이 addr과 offset은 PAGE SIZE의 배수여야한다고 했습니다. 

while 루프 안에서는 파일을 끝까지 복사할때까지 계속 반복합니다.

        while(fsz < sbuf.st_size){
        	//... 파일 복사 ...//
        }

while 루프 안의 로직은 어렵지 않습니다 .아까 배운 mmap을 통해서 src와 dst의 메모리를 대응 시킨 뒤에 memcpy 함수로 src의 내용을 dst의 내용으로 복사합니다.  그 이후 munmap으로 메모리를 해제하네요. 그 다음 page_size를 넘을만큼 그다면 다시 page_size 이후의 내용을 읽어야겠죠? fsz를 증가하는 이유가 그 이유입니다.

 

이상으로 메모리 대응 입출력에 대한 포스팅을 마치겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,