버퍼

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

와나진짜

,

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

와나진짜

,

 

DM-verity 

Data Mapper Verity의 약자로 쓰이는데, 루트 파일 시스템(Root FS)의 검증에 쓰인다. 과거에 안드로이드에 쓰였지만 현재는 점차 임베디드에 쓰이기 시작한다. 기본 원리는 아래의 해시 트리(Hash-Tree)를 만드는 것인데, Data Block마다 Hash를 생성해서 가장 아래 level의 hash 노드를 만들기 시작하고 그 위 level의 hash 노드를 그 아래 level에 hash node를 이용하여 만들어 점차 트리를 완성시켜가는데, 이때 가장 맨 위의 Hash 노드를 Root Hash라고 하며 안전하게 보관되어져야한다. 이 Root Hash값을 이용해서 dm-verity를 검증해야하기 때문.   

이러한 Hash tree를 Merkel Tree라고도 한다. 

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

 

1. 블록 장치 생성 + 파일 시스템 설정

# truncate -s 10G data_partition.ext4
# mkfs -t ext4 data_partition.ext4
# mkdir mnt 
# mount -o loop data_partition.ext4 mnt/ 
# echo "hello" > mnt/one.txt 
# echo "integrity" > mnt/two.txt 
# umount mnt/ 
# truncate -s 100M hash_partition.verity

10G의 크기를 갖는 블록을 하나 생성하여 ext4 파일 시스템을 적용한다. 그리고 그 안에 one.txt, two.txt를 만들고 난 후 unmount를 한다.

이후 100M 크기의 hash의 값을 갖는 영역을 생성해낸다. 이곳에 dm-verity의 merkle-tree가 저장된다.

 

2. dm-verity format

# veritysetup -v --debug format data_partition.ext4 hash_partition.verity > root_hash.txt

 

root_hash.txt의 내용

# cryptsetup 2.2.2 processing "veritysetup -v --debug format data_partition.img hash_partition.img" 
# Running command format. 
# Allocating context for crypt device hash_partition.img. 
# Trying to open and read device hash_partition.img with direct-io. 
# Initialising device-mapper backend library. 
# Formatting device hash_partition.img as type VERITY. 
# Crypto backend (OpenSSL 1.1.1f  31 Mar 2020) initialized in cryptsetup library version 2.2.2. 
# Detected kernel Linux 5.13.0-52-generic x86_64. 
# Setting ciphertext data device to data_partition.img. 
# Trying to open and read device data_partition.img with direct-io. 
# Hash creation sha256, data device data_partition.img, data blocks 2621440, hash_device hash_partition.img, offset 1. 
# Using 4 hash levels. 
# Data device size required: 10737418240 bytes. 
# Hash device size required: 84557824 bytes. 
# Updating VERITY header of size 512 on device hash_partition.img, offset 0. 
VERITY header information for hash_partition.img 
UUID:               4da1ecb5-5111-4922-8747-5e867036d9de 
Hash type:          1 
Data blocks:        2621440 
Data block size:    4096 
Hash block size:    4096 
Hash algorithm:     sha256 
Salt:       	     f2790cf141405152cf61b6eb176128ad0676b41524dd32ac39760d3be2d495cf 
Root hash:          a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f71 
# Releasing crypt device hash_partition.img context. 
# Releasing device-mapper backend. 
# Closing read write fd for hash_partition.img. 
Command successful.

Root Hash(a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f71)의 내용은 trusted된 내용이어야하고, 안전하게 보관되어야한다.

 

3. dm-verity open

# veritysetup open \ 
>         data_partition.ext4 \ 
>         verity-test \ 
>         hash_partition.verity \ 
>         a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f71

data_partition.ext4의 mapper를 생성한다.

/dev/mapper/verity-test 가 생성된 것을 확인할 수 있다.

# mkdir mnt 
# mount /dev/mapper/verity-test mnt/ 
mount: /home/ubuntu/ext4/mnt: WARNING: source write-protected, mounted read-only.
# cat mnt/one.txt mnt/two.txt 
hello
integrity

read-only로 mount 되었다.

 

4. dm-verity verify

user-space에서 검증이 가능하다.

  • 검증 성공 시
# veritysetup verify \
> /dev/mapper/verity-test \
> hash_partition.verity \
> a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f71
#
  • 검증 실패 시

  hash의 값을 변경(끝 네자리 0f71 0f73)

# veritysetup verify \
> /dev/mapper/verity-test \
> hash_partition.verity \
> a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f73
Verification of root hash failed.

 

5. Corruption 발생 처리

dm-verity는 디스크를 binary level 수준으로 보호하기 때문에 한 비트라도 어긋나게 되면 corruption 발생 처리하며 mount되지 않는다.

disk를 read-write로 mount하는 것 만으로도 meta-data 변형이 일어나기 때문에 binary가 변경된다. 따라서 corruption을 일으키기 충분하다.

# umount mnt/ 
# veritysetup close verity-test 
# mount -o loop data_partition.ext4 mnt/ 
# umount mnt/ 
# veritysetup open \ 
>         data_partition.ext4 \ 
>         verity-test \ 
>         hash_partition.verity \ 
>         a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f71 
Verity device detected corruption after activation 
root# mount /dev/mapper/verity-test mnt/ 
mount: /path/to/mnt: can't read superblock on /dev/mapper/verity-testroot## mount /dev/mapper/verity

 

dmesg를 확인하면 kernel에서 dm-verity의 log를 확인 할 수 있다.

[412036.212897] device-mapper: verity: 7:16: data block 0 is corrupted 
[412036.212996] device-mapper: verity: 7:16: data block 0 is corrupted 
[412036.213009] buffer_io_error: 91 callbacks suppressed 
[412036.213011] Buffer I/O error on dev dm-0, logical block 0, async page read 
[412036.223697] device-mapper: verity: 7:16: data block 0 is corrupted

 

5. dm-verity status

현재 mapping 상태를 확인한다.

# veritysetup status verity-test
/dev/mapper/verity-test is active.
  type:        VERITY
  status:      corrupted
  hash type:   1
  data block:  4096
  hash block:  4096
  hash name:   sha256
  salt:        f2790cf141405152cf61b6eb176128ad0676b41524dd32ac39760d3be2d495cf
  data device: /dev/loop11
  data loop:   /home/ubuntu/ext4/data_partition.ext4
  size:        20971520 sectors
  mode:        readonly
  hash device: /dev/loop10
  hash loop:   /home/ubuntu/ext4/hash_partition.verity
  hash offset: 8 sectors
  root hash:   a2a8fd07889deb10b4cdf53c01637ed373212cd7d0877a8aa9ae6fd4240f0f71

 

6. dm-verity close

mapping을 제거한다.

# veritysetup close verity-test

/dev/mapper에서 사라진것을 확인할 수 있다.

 

Reference)

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

https://www.starlab.io/blog/dm-verity-in-embedded-device-security

반응형
블로그 이미지

REAKWON

와나진짜

,

디바이스 드라이버에 Argument 전달

디바이스 드라이버가 등록될때나 실행되고 있을때 인자를 전달할 수 있을까요? 예를 들면 우리가 ls 명령어를 실행할때 ls -l /etc/와 같이 -l /etc/와 같은 인자들을 전달하는 것처럼 말이죠. 응용 프로그램에서는 간단합니다. 우리는 알고 있죠. C언어를 사용한다면 main에서 argc와 args를 사용해서 목적을 달성 할 수 있다는 것을 말이죠. 

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

 

리눅스의 디바이스 드라이버에서도 가능합니다. 바로 아래의 매크로들을 이용하면 됩니다. 포스팅 아래에서는 아래의 매크로를 통해서 예제 코드를 구현합니다.

  • module_param(name, type, perm) : 변수 name을 설정합니다. name의 자료형은 type이고, perm은 권한을 나타냅니다.
  • module_param_array(name, type, num, perm) : 배열 버전입니다. num이 자료형의 배열 크기를 나타냅니다.
  • module_param_cb(name, &ops, &name, perm) : 만약 paremeter의 값이 바뀐 경우 알아차려야한다면, 이 매크로를 사용하면 됩니다. cb는 callback의 약자입니다.

 

그리고 이러한 파라미터를 읽거나 수정할때 권한(permission)을 지정할 수 있습니다. S_I를 Prefix로 하고, R(Read), W(Write), X(eXecute)를 의미하며 USR은 user, GRP는 group 권한을 의미합니다. |(OR)을 통해서 여러 권한을 줄 수 있습니다.

  • S_IRUSR
  • S_IWUSR
  • S_IXUSR
  • S_IRGRP
  • S_IWGRP
  • S_IXGRP

 

아래의 코드를 보면서 어떻게 위의 매크로들이 사용되는지 알아보도록 하겠습니다. 


소스코드 

//passing_params.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>


int value, arr_value[4];
char *name;
int cb_value = 0;

module_param(value, int, S_IWUSR|S_IRUSR);
module_param(name, charp, S_IRUSR|S_IWUSR);
module_param_array(arr_value,int, NULL, S_IRUSR|S_IWUSR);

// module_param_cb를 위한 setter
int notify_param(const char *val, const struct kernel_param *kp){
        int res = param_set_int(val, kp);
        if(res == 0){
                printk(KERN_INFO "Call back function called...\n");
                printk(KERN_INFO "New value of cb_value = %d\n",cb_value);
                return 0;
        }
        return -1;
}

const struct kernel_param_ops my_param_ops = {
        .set = &notify_param, //위에 정의한 setter
        .get = &param_get_int, // standard getter
};

module_param_cb(cb_value, &my_param_ops, &cb_value, S_IRUGO|S_IWUSR);

static int __init my_module_init(void){
        int i;
        printk(KERN_INFO "===== Print Params =====\n");
        printk(KERN_INFO "value = %d \n", value);
        printk(KERN_INFO "cb_value = %d \n", cb_value);
        printk(KERN_INFO "name = %s\n", name);
        for(i = 0; i < sizeof(arr_value)/sizeof(int); i++){
                printk(KERN_INFO "arr_value[%d] = %d \n", i, arr_value[i]);
        }
        return 0;
}

static void __exit my_module_exit(void){
        printk(KERN_INFO "Kernel Module Removed ...\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Reakwon");
MODULE_DESCRIPTION("A simple parameter test module");
MODULE_VERSION("1:1.0");

 

Makefile

obj-m += passing_params.o
KDIR = /lib/modules/$(shell uname -r)/build

all :
        make -C $(KDIR) M=$(shell pwd) modules

clean :
        make -C $(KDIR) M=$(shell pwd) clean

insmod로 parameter 전달

$ sudo insmod passing_params.ko value=4 name="reakwon" arr_value=1,2,3,4

dmesg를 확인하면 잘 전달되고 읽히는 것을 확인할 수 있습니다.

insmod parameter 전달

 


파라미터 업데이트시 Callback

만약 parameter를 디바이스 드라이버가 실행 중일때 변경하고 이에 따른 동작을 수행하고자 한다면 어떻게 할까요? module_param_cb가 이런 이유때문에 존재합니다. 

모듈의 parameter는 /sys/module 아래의 자신의 모듈 이름의 디렉토리 하위 parameters 디렉토리에서 관리가 됩니다. 확인해볼까요? 

$ ls /sys/module/passing_params/parameters/
arr_value  cb_value  name  value

 

그래서 parameters의 하위의 변수들의 값을 바꿀수 있습니다. 이때 callback 함수를 등록하면 호출이 되게 됩니다.  위의 전체 코드 중 이와 관련한 코드가 여깄습니다.

// module_param_cb를 위한 setter
int notify_param(const char *val, const struct kernel_param *kp){
        //...//
}

const struct kernel_param_ops my_param_ops = {
        .set = &notify_param, //위에 정의한 setter
        .get = &param_get_int, // standard getter
};

module_param_cb(cb_value, &my_param_ops, &cb_value, S_IRUGO|S_IWUSR);

 

kernel_param_ops가 callback을 관리하는 구조체이고 여기의 멤버로 .set, .get, .free가 있습니다. 

struct kernel_param_ops 
{
 int (*set)(const char *val, const struct kernel_param *kp);
 int (*get)(char *buffer, const struct kernel_param *kp);
 void (*free)(void *arg);
};

 

아래와 같이 parameter의 값을 변경해봅시다. 

$ sudo sh -c "echo 1010 > /sys/module/passing_args/parameters/cb_value"

 

그리고 dmesg를 통해서 kernel 메시지를 확인하면 우리가 등록한 notify_param Callback함수가 호출됨을 알 수 있습니다. 

callback 호출

 

 

이 포스팅은 embetronicx의 contents를 참고하여 작성된 포스팅입니다. 여기에 다 담지못하는 부분은 아래의 페이지에서 참고하시기 바랍니다. 앞으로도 embetronicx의 tutorial을 기반으로 작성할 예정입니다.

https://embetronicx.com/tutorials/linux/device-drivers/linux-device-driver-tutorial-part-3-passing-arguments-to-device-driver/

 

반응형
블로그 이미지

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

와나진짜

,

MsgSend, MsgReceive, MsgReply를 이용한 client - server sample

MsgSend는 함수명만 보면 보내기만 할것 같지만, 실제로는 받는것도 하고 있다. Client - Server 모델에서 echo 서버 형태를 구현한다고 하면 client쪽에서는 send 이후 receive로 받아오는데, 이때 receive할때까지 client는 기다린다. MsgSend는 send, receive를 합한 형태로 보면 된다. 그렇기 때문에 MsgSend는 데이터를 받아올때까지 Blocking되는 특성이 있다. 함수 원형은 아래와 같다. 

#include <sys/neutrino.h>

int MsgSend (int coid,
             const void *smsg,
             int sbytes,
             void *rmsg,
             int rbytes);

 

coid : ConnectAttach()를 통해서 나온 값

smsg : 보내는 데이터

sbytes : 보내는 데이터의 길이

rmsg : 받는 데이터

rbytes : 받는 데이터의 길이 

 

MsgReceive는 데이터를 받는 함수이고, MsgReply는 응답을 하는 함수이다. sample을 보면 이해가 갈것이다.

//Server.c 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/neutrino.h>

#define EOK 1234
int main(void)
{
    int     rcvid;         // indicates who we should reply to
    int     chid;          // the channel ID
    char    message [512]; // big enough for our purposes

	printf("Server Start\n");

    // create a channel
    chid = ChannelCreate (0);
	printf("Wait in channel %d\n",chid);

    // this is typical of a server:  it runs forever
    while (1) {

        // get the message, and print it
        rcvid = MsgReceive (chid, message, sizeof (message),
                            NULL);
        printf ("Got a message, rcvid is %X\n", rcvid);
        printf ("Message was \"%s\".\n", message);

        // now, prepare the reply.  We reuse "message"
        strcpy (message, "This is the reply");
        MsgReply (rcvid, EOK, message, sizeof (message));
    }

	return 0;
}

서버쪽에서는 ChannelCreate로 chid를 가져오고, Client는 여기에 Attach한다. 

//client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/neutrino.h>

int main(int argc, char *argv[]){
	char *smsg = "This is the outgoing buffer";
	char rmsg [200];
	int coid;
	int ret;
	pid_t pid;

	if(argc != 2){
		fprintf(stderr,"Usage : %s [server_pid] \n",argv[0]);
		exit(EXIT_FAILURE);
	}
	pid = atoi(argv[1]);
	// establish a connection
	coid = ConnectAttach (0, pid, 1, 0, 0);
	if (coid == -1) {
		fprintf (stderr, "Couldn't ConnectAttach to %d/%d/%d!\n", 0, pid, 1);
		perror (NULL);
		exit (EXIT_FAILURE);
	}

	// send the message
	if ((ret  = MsgSend (coid,
				smsg,
				strlen (smsg) + 1,
				rmsg,
				sizeof (rmsg))) == -1) {
		fprintf (stderr, "Error during MsgSend\n");
		perror (NULL);
		exit (EXIT_FAILURE);
	}

	if (strlen (rmsg) > 0) {
		printf ("Process ID %d returns code :%d, msg :\"%s\"\n", pid, ret, rmsg);
	}
}

 

여기서 ConnectAttach의 channel id는 1로 임의로 지정했는데, argv로 입력받는 것이 더 좋을듯하다. 난 귀찮아서...

반응형
블로그 이미지

REAKWON

와나진짜

,

나의 중이염 일대기

7살, 8살 쯔음 혼자서는 똥, 오줌도 못가릴 무렵, 귀 속이 찢어진 듯한 통증과 함께 진물이 나온 기억이 있는데, 이때 방치했던 것이 만성 중이염으로 이어졌다. 중이염이라는게 콧물 등에 포함된 세균이 유스타키오관을 통해 중이 안으로 침투해 염증을 일으키는 질환으로 진물이 고막 안에 고여있는데, 이때 느낌이 귀에 물이 찬 느낌이다. 그러다가 이게 마침내 터지면 귀에 진물이 나오게 된다. 고막에 구멍이 뚫리니 통증을 당연이 느끼게 되는것이다. 특히 이 유스타키오관이 짧은 어린애들이 잘걸리니 아이들이 아무이유없이 울고있다면 궁디팡하지말고 이비인후과로 데려가도록 하자. 

https://post.naver.com/viewer/postView.nhn?volumeNo=7842154&memberNo=24304009

 

암튼 12살까지 방치하다가 12살~13살까지 동네 이비인후과 병원을 다니기 시작했다. 몇년을 방치하니까 튜브를 삽관하는 수술을 했는데, 이때 삽관이 잘 되지 않아 대학병원에서 수술을 받았다. 14살이 되어서 아주대학교 병원에서 수술을 진행했고 귀 안에 튜브를 박고 1년 동안 통원 치료를 받았는데, 좀 처럼 청력을 돌아오지 않았다. 이렇게 튜브 삽관 수술을 18살때까지 진행하다가 더이상 진전되지 않은 것 같아 21살까지 방치하다가 군대를 가게 되었다.

이때부터는 거의 자포자기다. 청력은 한쪽에만 의지하며 이후 +10년 추가. 합 20년 가까이 중이염을 앓고 지냈는데, 최근 이명과 함께 귀가 먹먹해지는 느낌이 들어 대학 병원이 아닌 귀 전문 병원으로 진료를 받으러갔다. 소리이비인후과가 유명하다고 해서  무려 청담으로 향했다. 1층을 제외한 건물 자체가 병원이다..

 

검사

가게 되면 우선 귀를 한번 째본다. 긴 뾰족한 꼬챙이로 귀의 고막을 한번 뚫는데, 이 이유는 안에 물이 찼는지, 액체형태로 빠져나오는지, 아니면 나오지 않는지 확인해보는 거라고 한다. 난 이 꼬챙이를 대학병원을 다니면서 몇 차례 당한적이 있어 보기만해도 몸에서 경기가 일어났다.

나 : "헑엉 ㅠㅠ 저 이거 안하면 안돼요? 이거 엄청 아픈데...ㅜㅜ "

의사 : "안아프게 할게요. 살짝만"

결국 살짝 뚫었는데, 그렇게 아프게 하지 않았다. 역시 다년간의 경험으로 처음부터 엄살을 떨어야만 의사샘이 살살해준다. 여러분도 일단 개거품부터 물어보자.

나는 진물이 나오지 않았다. 이것은 즉, 고름이 안에 진득진득하게 차있어서 그런것이다. 이제 청력검사를 시행한다. 청력검사는 별거없는데, 삐 소리가 나면 버튼을 누르는 식, 그리고 귀 안에 청신경?이 살아있나 보는 진단을 하게 된다. 별거없으니 패스

나는 고막이 안으로 말려들어간 유착성, 그리고 안에 고름이 차있는 진주종성 중이염 판정을 받고 수술을 하는 것으로 판정이 났다.  수술을 하기위한 피, 소변검사는 소리이비인후과 병원에서 해주는데, 나머지 X-ray 사진, 심전도 검사는 내과에서 하고 팩스로 전달해야한다.

 

입원은 총 아래와 같이 3일하게 된다. 무적권 1인실 밖에 없고, 청담이라 1박에 무려 24만원이다. 시간당 만원꼴.

 

수술은 총 3가지를 진행하는데 아래와 같다.

1. 유양동 절제술

2. 고막 성형술

3. 이소골 재건술

이 수술들을 진행하기 위해서는 귀구멍 안으로 수술하는게 아니라 귀 뒤 절개한 다음 진행해야하는 수술이라 좀 무섭...긴해도 전신마취라서 자고 일어나면 끝나있다... 라고 생각하는게 정신건강에 좋다. 스스로를 가스라이팅하길 바란다. 

이후 2차 수술도 할수도, 안할 수도 있다고 한다. 그건 1차 수술 결과를 보고 알 수 있나보다. 

 

입원(1일차)

드디어 그날이 되어 상큼하게 병원을 가서 접수를 한다. 뭔가 다리가 요염하게 나왔는데, 오줌을 참고 있었나보다. 여기 안내 데스크에 있는 간호사분들은 환자한테 먼저 인사 안하길래 내가 먼저했다. 날 못본거겠지? 그렇다고 내가 10곤대는 아니고... 

 

 

접수하고 나면 코로나 검사를 받고 여기서 대기좀 타야되는데 기다리는 시간동안 만성 중이염에 대해서 알아보자.

 

옼키, 대충 봤으면 이후 어지럼증 검사를 하고 입실하게 된다. 방안의 풍경은 이러하다.

 

 

 

8시까지는 환자가 돌아와야하고, 10시까지만 면회가 허용이 가능하다. 첫날은 저녁이 안나오니까 밖에서 먹고오거나, 싸오거나 해야한다. 오후 9시쯤 되어서 항생제 반응검사를 하고 항생제를 맞는다.

수술 당일(2일차)

나의 경우 수술은 전신마취를 통해서 진행했다. 오전 7시부터 수술실에 들어간것으로 기억하고, 마취전문의를 동반하여 수술을 진행한다. 그렇기 때문에 자고 일어나면 수술이 끝나있다. 아참! 수술 당일에 무조건 보호자가 한명이 필요하다. 

수술이 끝나고  난 후의 모습인데, 미간에 야무지게 인상을 쓰고 있는거보니, 다행히 살아있다. 사진은 내 사랑스러운 여자친구가 찍어주심.. 

 

음.. 이때부터는 이제 통증과의 싸움이긴 한데, 진통제를 맞으면 조금은 괜찮아지긴한다. 근데 아프면 너무 참지말고 진통제를 맞거나 먹는것이 더 좋다. 통증의 정도는 귀를 송곳으로 뚫은 정도의 통증이라고 보면 될것 같다.

수술이 끝난 직후 나의 증상은 몇가지 있었는데 나열하면 아래와 같다.

● 혀가 얼얼하다. 약간 미각이 안느껴진다. 수술후 하루가 지나도 그렇다.

● 목이 아프다. 전신 마취중에 기관지가 손상이 될 수도 있다고하는데, 그런건가보다. 목이 좀 아프다. 

● 어지러움은 없었다. 마취깨고 1시간 지나고 나서야 제정신이 들었다.

● 귀에 물흐는 소리와 물이 차있는 소리, 마치 고막이 움직이는 느낌이 든다. 고막 성형했으니 그런 소리가 나는것은 매우 정상이라한다. 

 

병원밥은 못참지

이 와중에 밥은 레알 맛있는데, 씹기가 어려운게 문제다. 

사진에 보이는거 파란색 머리띠 저게 진짜 답답하긴 한데, 그 안에 거즈로 피를 막아놓고 고정해서 차고 있어야한다. 수술이 7시부터 9시 살짝 넘어서 끝났고, 이 거즈는 세시 정도에 의사샘이 갈아준다. 이때 본인 피 보고 놀라지 마시길...

나같은 경우에는 시간이 지날수록 통증이 좀 심해졌다. 그렇지만 저녁먹고 진통제 맞고, 새벽에 아플때 진통제 먹고 그러니 참을만은 했다.

오늘 하루 종일 나 챙겨주고 간호해준 여자친구, 너한테 평생잘해주께 영원히. 사랑한다 베이비♥♥♥♥

 

퇴원(3일차)

오후 6시에 상처부의를 소독해주고, 거즈대신 살색 의료용 테이프같은거로 갈아준다. 9시에 의사샘이 다시 오시는데, 별건 없다. 어디 불편한지, 아픈지 묻고 퇴원하면 된다. 입원비.. 188만원 나왔다. 18

이제부터 7주일동안은 상처를 잘 아물게 최대한 노력을 해야하는데, 항생제는 식후 30분 3끼 다먹고 소화제도 같이 먹는다. 아플때는 진통제를 먹으면 된다. 나같은 경우에는 하루에 두번 잘 챙겨먹는다. 

사진에서 보면 가장 오른쪽 위에는 귀 외이도에 넣어주는 지혈솜? 같은건데, 새로로 꽂아넣고 살색 테이프로 귀에 붙여줘야 나중에 갈때 편하다. 

 

수술후 주의사항 중에서도 술, 담배는 안되는 것은 다 알고 있겠지만 물을 특히 조심해야한다. 오히려 안씨는것을 추천해줄 정도로 물을 닿게해서는 안된다. 특히 상처부위말고 귀구멍안에 물은 진짜 들어가면 안된다고한다. 조심해야지~~~

여기까지 나의 중이염 수술 과정과 후기를 적었는데, 지금 이 시점은 퇴원하고 막 글을 쓰는 시점이라서 수술 결과는 모른다. 근데 의사샘이 수슬은 잘됐다고 알려주셨는데, 이제 잘 관리하고 2차 후기를 남길 수 있도록 해야지. 이 글을 끝까지 본 사람이면 중이염 수술을 생각하고 있는 분일텐데, 너무 무섭게 생각치 마시고 용기 내서 완치하세요.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

readn, writen

네트워크같은 환경에서 어마어마한 데이터가 쓰여질 경우 일반 read함수로는 전부 읽어올 수 없는 상황이 발생할 수 있습니다. 또 반대의 경우에도 마찬가지입니다. 큰 데이터가 한번에 쓰여질 경우 write함수로는 쓰여지지 않을 수 있습니다. 아래와 같은 두가지의 상황이 발생하기 때문인데요.

1) read 연산이 요청보다 작은 바이트를 되돌려줄 경우에는 이는 오류가 아닙니다. 단지, 더 읽어야할 데이터가 있다는 뜻입니다. 2) write 역시 요청보다 작은 바이트를 돌려줄 수 있는 경우가 있는데, 커널의 출력 버퍼가 꽉 찼을때 이런 경우가 발생합니다.

1), 2)는 어떻게 해결할 수 있을까요? 답은 데이터가 요청된 값이 될때까지 읽거나 쓰는 것입니다. 이런 함수가 아래의 readn, writen 함수입니다.

- 아래의 코드는 Advanced Programming in the UNIX Environment 3판을 참고하여 만든 코드입니다.

 rean 함수

ssize_t readn(int fd, void *data, size_t n){
        size_t left;    //남은 바이트
        size_t read_n;  //읽은 바이트

        left = n;

        while(left > 0){
                if((read_n = read(fd, data, left)) < 0){
                        if(left == n) return -1;
                        else break;
                }else if(read_n == 0) break;
                left -= read_n; //얼마나 남았는지 갱신
                data += read_n; //남은 바이트 읽기 위해 포인터 이동
        }

        return n-left;
}

 

readn 함수를 보면 while문을 통해서 계속 다 읽을때까지 read를 호출하는 것을 볼 수 있습니다. read가 0을 반환하면 다 읽었다고 판단하여 while 루프를 종료하고 빠져나오면 됩니다.

 

writen 함수

ssize_t writen(int fd, const void *data, size_t n){
        size_t left;    //쓰기까지 남은 바이트
        ssize_t written_n;      //쓴 바이트

        left = n;

        while(left > 0){
                if((written_n = write(fd, data, left)) <0){
                        if(left == n) return -1;
                        else break;
                }else if(written_n == 0) break;
                left -= written_n;      //얼마나 남았는지 갱신
                data += written_n;      //남은 바이트 쓰기 위해 포인터 이동
        }

        return n-left;
}

 

writen 함수 역시 계속 data를 쓸때까지 while 루프로 계속 write를 호출하는 것을 볼 수 있습니다. write가 0을 반환하게 되면 다 썼다는 의미로 반복문을 빠져나오고 writen 함수가 종료됩니다.

 

readn, writen 함수는 read, write함수와 동일하게 사용이 가능합니다. 이 함수들은 socket과 같은 네트워크, 파이프, FIFO 등에서 대량의 바이트를 읽거나 쓰기위한 함수라는 것을 기억하시기 바랍니다. 일반 파일 IO에서는 read, write만 사용해도 무관합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

리눅스 네트워크 프로그래밍에 관한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

netstat

네트워크의 connection, routing table, interface 통계 등 네트워크 전반의 정보들을 나타내어주는 명령어입니다. netstat 명령어를 사용함으로써 시스템의 port들이 어떤 상태인지를 확인할 수 있게 됩니다. 예를 들어 현재 서버에서 socket이 열려서 client의 연결을 대기 중인 상태라면 listen 상태가 됩니다. 

netstat을 어떻게 사용하는 지 알아봅시다.

기본적으로 netstat만을 사용하면 열린 모든 socket들에 대해서 보여줍니다. 그런데 기본적으로 LISTENING 상태는 보여주지 않습니다. LISTENING 상태의 socket을 함께 보고싶으시면 아래의 설명할 -a, 혹은 -l 옵션을 지정해주어야합니다.

 

※  여기서 Active Internet connections, Active UNIX domain sockets라고 하여서 약간 다르게 나타나는게 보이시죠? Internet connections는 여러분이 알고 있는 네트워크 상의 connection을 의미하지만, UNIX Domain Sockets(UDS라고도 합니다.)은 시스템 내부에서 사용하는 socket으로 인터넷을 사용하지 않고 통신하기 위한 IPC 기법입니다. socket 파일을 통해서 통신합니다. ls -l 명령을 입력했을때 앞에 s가 바로 그 socket 파일입니다. 복잡한 네트워크를 사용하지 않기 때문에 아래에서 보겠지만 State가 Internet connections보다는 간단합니다. 함수는 인터넷 socket과 동일하게 사용합니다. 

 

netstat를 그냥 사용하면 정신없을 수도 있으니까 아래의 option들을 적절히 이용해서 사용하도록 합시다.

옵션 설명
-l Listening 중인 socket을 표시합니다.
-p socket을 사용하는 pid와 program 이름을 보여줍니다.
-n 주소등을 number로 표시합니다(ex localhost를 127.0.0.1 로 표현)
-i interface의 정보를 보여줍니다.
-t tcp 사용 socket을 보여줍니다.
-u udp 사용 socket을 보여줍니다.
-r routing table을 보여줍니다.
-a listening과 non-listening 상태 모두를 보여줍니다.
-c 매 초마다 명령을 계속적으로 실행합니다.

 

netstat -nltp : listen 중인 tcp를 사용하는 socket을 표시하는데, 그 socket을 사용하는 프로그램도 같이 표시. 주소를 숫자로 표현 

 

netstat -nltp | grep port 번호 : 특정 port가 어떤 상태인지 알아보려면 grep을 이용해서 찾을 수 있습니다.

 

netstat -r : 라우팅 테이블의 정보를 확인할 수 있습니다.

 

 

State

netstat을 사용하면 State라는 socket의 상태를 볼 수 있는데, Internet connection과 UDS를 아래의 표로 정리하였습니다.

- Internet 

State 설명
ESTABLISHED socket이 연결이 성립된 상태입니다.
SYN_SENT socket이 syn 패킷을 보냈으며 연결을 시도하려고 하는 상태입니다.
SYN_RECV socket이 연결 요청을 받은 상태입니다.
FIN_WAIT1 socket이 닫혔고, 연결이 해제되고 있는 상태입니다.
FIN_WAIT2 연결이 닫혔으며, socket은 remote end로부터 shutdown을 기다리고 있는 상태입니다.
TIME_WAIT close 이후 socket이 여전히 네트워크에 남아있는 패킷을 처리하기 위해  대기 중인 상태입니다. 일정시간이 지난 후 이 상태는 사라집니다.
CLOSE socket이 사용중이지 않는 상태입니다.
CLOSE_WAIT remote end가 연결을 해제하였고, 이 컴퓨터의 socket이 닫히기를 기다리고 있는 상태입니다. 
LAST_ACK remote end에서 shut down되었고, socket이 닫힌 상태입니다. 하지만 최종 ack 응답은 기다리는 상태입니다.
LISTEN socket이 연결을 위해서 listening 중인 상태입니다. 
CLOSING 양쪽 모두 socket이 닫혔지만, 페킷을 완전히 못받은 상태입니다, 즉, 페킷이 유실되었다는 의미입니다.
UNKNOWN 어떠한 이유에서 socket의 상태를 알수가 없는 상태입니다.

 

모두 알필요는 없으며, LISTEN, ESTABLISHED, WAIT과 관련된 것만 봐도 상관없을 것 같네요.

 

- Unix Domain Socket(UDS)

State 설명
FREE socket이 할당되지 않았습니다.
LISTENING connection을 대기하고 있습니다.  internet의 LISTENING과 같습니다.
CONNECTING connection을 막 하고 있는 상태입니다.
CONNECTED 연결이 된 상태입니다. internet의 ESTABLISHED 상태와 같습니다.
DISCONNECTING socket이 연결 해제 중입니다.
(empty) socket이 연결되지 않은 상태입니다.
UNKNOWN 절대로 발생할 수 없는 상태입니다. 

 

하지만 netstat은 오래된 프로그램이라서 ss 명령어로 대체가 되었습니다. 나중에 시간이 있으면 ss 명령어에 대해서 포스팅하도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,