버퍼

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

와나진짜

,