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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

리눅스 데몬(Daemon)

백그라운드에서 실행되는 일종의 프로세스이며 오랫동안 유지되는 프로세스입니다. 주로 시스템이 시작될때 같이 시작이 되서 시스템이 종료될때 같이 종료되는 경우가 대부분입니다. 백그라운드에서 돌면서 사용자가 원하는 서비스를 제공하는 프로세스를 의미합니다. 데몬은 일반 백그라운드 프로세스와는 다르게 제어 터미널이 없고 표준 입출력이 없습니다. 즉, 사용자로부터 직접적인 입력를 받지 않고 출력도 하지 않습니다.

데몬의 예는 뭐가 있을까요? 관례상 데몬은 프로세스 이름이 d로 끝납니다. 예를 들어 네트워크 인터페이스들을 감시하고 서버로부터 요청을 감지하는 inetd, 설정된 날짜와 시간이 되면 지정된 명령을 실행시켜주는 cron, 보안 원격 로그인을 제공하는 sshd와 같은 것들이 있지요. 이러한 데몬을 생각해보세요. 사용자가 입력하지도, 출력을 내보내지도 않습니다.

ps명령어를 통해서 어떤 데몬이 있는지 보도록 해봅시다.

   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      0       1       1       1 ?             -1 Ss       0   1:42 /sbin/init splash
      0       2       0       0 ?             -1 S        0   0:00 [kthreadd]
      2       3       0       0 ?             -1 I<       0   0:00 [rcu_gp]
      2       4       0       0 ?             -1 I<       0   0:00 [rcu_par_gp]
      2       6       0       0 ?             -1 I<       0   0:00 [kworker/0:0H-kblockd]
      2       9       0       0 ?             -1 I<       0   0:00 [mm_percpu_wq]
      2      10       0       0 ?             -1 S        0   0:17 [ksoftirqd/0]
      2      11       0       0 ?             -1 I        0   0:51 [rcu_sched]
      2      12       0       0 ?             -1 S        0   0:01 [migration/0]
      2      13       0       0 ?             -1 S        0   0:00 [idle_inject/0]
      2      14       0       0 ?             -1 S        0   0:00 [cpuhp/0]
      2      15       0       0 ?             -1 S        0   0:00 [kdevtmpfs]
      2      16       0       0 ?             -1 I<       0   0:00 [netns]
      2      17       0       0 ?             -1 S        0   0:00 [rcu_tasks_kthre]
      2      18       0       0 ?             -1 S        0   0:00 [kauditd]
      2      19       0       0 ?             -1 S        0   0:00 [khungtaskd]
      2      20       0       0 ?             -1 S        0   0:00 [oom_reaper]
      2      21       0       0 ?             -1 I<       0   0:00 [writeback]
      2      22       0       0 ?             -1 S        0   0:00 [kcompactd0]
      2      23       0       0 ?             -1 SN       0   0:00 [ksmd]
      2      24       0       0 ?             -1 SN       0   0:00 [khugepaged]
      2      70       0       0 ?             -1 I<       0   0:00 [kintegrityd]
      2      71       0       0 ?             -1 I<       0   0:00 [kblockd]
      2      72       0       0 ?             -1 I<       0   0:00 [blkcg_punt_bio]
      2      73       0       0 ?             -1 I<       0   0:00 [tpm_dev_wq]
      2      74       0       0 ?             -1 I<       0   0:00 [ata_sff]
      2      75       0       0 ?             -1 I<       0   0:00 [md]
      2      76       0       0 ?             -1 I<       0   0:00 [edac-poller]
      2      77       0       0 ?             -1 I<       0   0:00 [devfreq_wq]
      2      78       0       0 ?             -1 S        0   0:00 [watchdogd]
      2      83       0       0 ?             -1 S        0   0:01 [kswapd0]
      2      84       0       0 ?             -1 S        0   0:00 [ecryptfs-kthrea]
      2      86       0       0 ?             -1 I<       0   0:00 [kthrotld]
      2      87       0       0 ?             -1 I<       0   0:00 [acpi_thermal_pm]
      2      88       0       0 ?             -1 S        0   0:00 [scsi_eh_0]
      2      89       0       0 ?             -1 I<       0   0:00 [scsi_tmf_0]
      2      90       0       0 ?             -1 S        0   0:00 [scsi_eh_1]
      2      91       0       0 ?             -1 I<       0   0:00 [scsi_tmf_1]
      2      93       0       0 ?             -1 I<       0   0:00 [vfio-irqfd-clea]
      2      95       0       0 ?             -1 I<       0   0:00 [ipv6_addrconf]
      2     104       0       0 ?             -1 I<       0   0:00 [kstrp]
      2     107       0       0 ?             -1 I<       0   0:00 [kworker/u3:0]
      2     120       0       0 ?             -1 I<       0   0:00 [charger_manager]
      2     121       0       0 ?             -1 I<       0   0:37 [kworker/0:1H-kblockd]
      2     166       0       0 ?             -1 S        0   0:00 [scsi_eh_2]
      2     167       0       0 ?             -1 I<       0   0:00 [scsi_tmf_2]
      2     187       0       0 ?             -1 S        0   0:15 [jbd2/sda5-8]
      2     188       0       0 ?             -1 I<       0   0:00 [ext4-rsv-conver]
      1     227     227     227 ?             -1 S<s      0   0:11 /lib/systemd/systemd-journald
      1     252     252     252 ?             -1 Ss       0   0:26 /lib/systemd/systemd-udevd

 

데몬 코딩 룰

1. 데몬이 파일을 생성할때 적절한 권한이 있어야하는데 umask를 사용하여 파일 모드 생성 마스크를 설정합니다. 보통 0입니다. umask는 생성되는 파일의 생성모드를 정해주는데 umask가 현재 0x022라면 group의 w, other의 w 권한을 주지 않는 다는 것입니다. 따라서 파일을 rwxrwxrwx로 생성한다 하더라도 rwxr-xr-x로 생성이 된다는 겁니다. 

데몬이 파일을 올바르게 생성해야만 하므로 umask(0)을 지정합니다.

2. 자식 프로세스를 생성(fork)한 후 부모 프로세스는 종료(exit)합니다. 이렇게 자식 프로세스보다 부모 프로세스가 먼저 종료되면 initd이 자식 프로세스를 넘겨받게 됩니다. 자식 프로세스는 세션의 리더가 아니며 프로세스 그룹 리더도 아닙니다.

3. setsid를 호출해서 새로운 세션을 생성합니다. setsid는 다음과 같은 세가지 일이 일어납니다.


#include <unistd.h>
pid_t setsid(void);

호출 프로세스가 프로세스 그룹 리더가 아니면 새 세션을 생성합니다.
1. 호출 프로세스가 새 세션의 리더가 됩니다. 
2. 호출 프로세스는 새 프로세스 그룹의 리더가 됩니다. 
3. 호출 프로세스는 제어 터미널이 없습니다.

4. 현재 작업 디렉토리를 루트 디렉토리로 변경합니다. 

5. 필요하지 않은 모든 파일 디스크립터를 닫습니다. 어떻게 열려진 파일 디스크립터를 모두 알 수 있을까요? getrlimit이나 open_max를 사용하면 됩니다.

6. 표준 입출력을 모두 /dev/null로 지정합니다. 데몬은 사용자로부터 입력을 받을 필요가 없고, 출력, 에러를 직접 하지 않습니다. 데몬에는 제어 터미널 장치가 연관되어 있지 않고 출력이 표시될 대상이나 사용자로부터 입력을 받을 통로가 없습니다.

데몬 코딩

위의 코딩 룰 대로 코딩한 데몬 프로그램입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <syslog.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

void daemonize(const char *cmd){
        int i,fd0,fd1,fd2;
        pid_t pid;
        struct rlimit rl;
        struct sigaction sa;

        //umask를 0으로
        umask(0);

        if(getrlimit(RLIMIT_NOFILE,&rl)<0){
                printf("%s: can't get file limit\n",cmd);
                exit(0);
        }

        //자식 프로세스 생성 후 부모는 종료
        if((pid=fork())<0){
                printf("%s: can't fork\n",cmd);
                exit(0);
        }else if(pid !=0)
                exit(0);

        //세션을 만들고 리더가 됨.
        setsid();

        //SIGHUP 무시, 제어터미널 연결이 끊겨도 무시
        sa.sa_handler=SIG_IGN;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags=0;
        if(sigaction(SIGHUP,&sa,NULL)<0){
                printf("%s: can't ignore SIGHUP\n",cmd);
                exit(0);
        }
        if((pid=fork())<0){
                printf("%s:can't fork\n",cmd);
                exit(0);
        }
        else if(pid!=0)
                exit(0);


        // 작업 디렉토리를 /로 변경
        if(chdir("/") < 0){
                printf("%s: can't change directory to /\n",cmd);
                exit(0);
        }

        //열려있는 파일 디스크립터 닫기
        if(rl.rlim_max == RLIM_INFINITY)
                rl.rlim_max = 1024;
        for(i=0;i<rl.rlim_max;i++)
                        close(i);

        //모두 닫았으니 fd는 0부터 return되며 모두 /dev/null로 직행
        fd0=open("/dev/null",O_RDWR);
        fd1=dup(0);
        fd2=dup(0);

        openlog(cmd,LOG_CONS,LOG_DAEMON);
        if(fd0 != 0 || fd1 != 1 || fd2 != 2){
                syslog(LOG_LOCAL0," unexpected fd : %d %d %d",fd0,fd1,fd2);
                exit(1);
        }

        syslog(LOG_LOCAL0,"helloworld! [%s]start",cmd);
}


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

        //100초후 데몬 종료
        sleep(100);
}

 

이후에 compile하고 실행시켜봅시다. 


# gcc daemon.c -o dd
# ./dd

아무 변화가 없죠. 이제 ps -axj로 실행시켜보세요.


# ps -efj
...
      1  105975  105974  105974 ?             -1 S        0   0:01 ./dd
 104369  105977  105977  103273 pts/2     105977 R+       0   0:00 ps -axj
...

 

./dd라는 프로세스가 보이네요. 만약 정상적인 프로세스라면 부모 pid도 같이 보여야합니다. 찾아볼까요?


# ps -axj | grep 105974 
      1  105980  105974  105974 ?             -1 S        0   0:01 ./dd

없음을 알 수 있네요. 부모프로세스는 105974인데 먼저 종료가 되었죠. 그렇기 때문에 initd가 그 프로세스를 짬처리 당했고 자식 프로세스는 setsid로 새로운 세션의 리더가 됩니다. 

이러한 사실들을 보면 daemon이 올바르게 초기화되었다는 것을 알 수 있습니다.

 

로그 기록

데몬은 입출력 통로가 없기 때문에 사용자에게 특정한 사실을 알리기 위해서는 파일에 기록하는 식으로 알리게 됩니다.  이때 만약 데몬마다 로그파일이 다르다면 시스템 관리자 입장에서는 어떤 로그파일이 무슨 데몬의 로그인지 확인하기가 까다롭게 됩니다.

그래서 로그를 중앙 집중적으로 관리하게 되고 이때 이 로그를 관장하는 데몬이 syslogd입니다. syslogd를 사용하기 위해서는 우선 syslog가 설치가 되어있어야겠죠. 

이후에는 /etc/syslog.conf 파일 맨 아래에 다음과 같은 설정을 합니다. 

local0.*        /var/log/dd_info

이제 변경사항을 반영하기 위해서 syslogd를 재시작합니다.

# /etc/init.d/syslog restart

 

이제 우리의 데몬을 실행시켜보도록 합시다. 이제 /var/log/dd_info에 우리의 로그가 나오는것을 확인할 수 있을 것입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

다중입출력 

아래와 같은 상황을 생각해볼까요? 어떤 프로세스가 다음과 같이 파일 3개를 처리하는데 그 중 입력이 있는 파일을 프로세스에 처리하는 상황을 어떻게 구현할 수 있을까요?

이때 우리는 쓰레드를 생각해볼 수 있겠지요. 나쁘지 않은 아이디어입니다. 그렇다면 파일 3개의 대해서 3개의 쓰레드를 돌려야하겠네요.

여러분도 알다시피 쓰레드는 많이듭니다. context switching이 대표적이죠. 그리고 파일 처리에 대해서 동기화 기법을 적용해야할 수도 있습니다. 상당히 복잡하고 어려운 구현이 될 수도 있습니다.

이제 우리는 스레드를 이용하는 방법말고 보다 경량적인 방법을 선택할 것인데 이러한 구현을 도와주는 system call이 바로 select, poll, epoll 입니다. 이 포스팅에서는 select에 대해서만 설명합니다.

1. select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tiemval *timeout)

- ndfs : 감시할 파일 디스크립터의 갯수인데 마지막 파일 디스크립터 번호에 +1을 지정해줍니다. fd_set은 배열인데 배열의 index는 0부터 시작하므로 +1을 해줍니다. 아래에서 설명하도록 하죠.

- readfds : 읽을 데이터가 있는지 감시하는 파일의 집합입니다.

- writefds : 파일에 데이터를 쓸 수 있는지를 검사하기 위한 파일 집합입니다.

- exceptfds : 파일의 예외 사항이 있는지 검사하기 위한 파일 집합입니다.

- timeout : 데이터의 변화를 감지하기 위해서 사용하는 time out 값입니다. 예를 들어 readfds에 지정된 시간동안 읽을 데이터가 없다면 select는 0을 반환합니다. 만약 timeout을 NULL로 지정하게 되면 무기한으로 대기하게 됩니다.

 

성공시 fd의 갯수를 반환합니다.

fd_set

fd_set은 아래와 같은 비트 배열입니다. 총 1024개의 파일을 감시할 수 있죠. 아래는 readfds를 표현했습니다. fd_set은 총 1024개의 fd를 감시할 수 있습니다. 

우리는 readfds에 0, 1, 2, 3을 read할 데이터가 있는지 감시한다고 해봅시다. 이때 fd 3에 read할 데이터가 있다면 위의 배열은 아래와 같이 fd 3에 해당하는 비트 배열의 flag가 1이 세팅됩니다. 이렇게 하여 fd 3에 읽을 데이터가 있는지 알 수 있습니다.

이때 fd 3은 마지막에 열었던 파일 디스크립터입니다. 그렇다면 지금까지 감시해야할 fd의 갯수는 몇개인지 나오지요? 마지막 fd의 번호 + 1이 됩니다. 그 때문에 nfds가 마지막 fd의 +1이 되는 것이죠.

 

그래요, 이제 fd를 감시하는 것은 알겠는데, 이것이 스레드처럼 동시에 fd를 관리할까요? 결론은 아닙니다. 만약 파일을 1024개를 관리하고 1023번 파일디스크립터(fd)에 read 플래그가 1이 되었다면 select는 모든 fd_set을 모두 검사하여 변화가 있는 fd를 알아내게 됩니다.

 

 

 

 

 

 

 

 

FD 매크로

보다 파일 디스크립터 집합(fd_set)을 관리하기 편하게 하기 위해서 리눅스에서는 다음과 같이 4개의 매크로를 지원합니다.

 

매크로 설명
void FD_CLR(int fd, fd_set *set) set에서 fd를 제거합니다. 더 이상 검사하지 않습니다.
int  FD_ISSET(int fd, fd_set *set) set에 fd의 플래그가 setting되었는지 확인합니다. 
void FD_SET(int fd, fd_set *set) set에 fd를 추가합니다. 이것은 flag를 setting하는 것이 아닌 검사를 하겠다는 뜻의 setting입니다.
void FD_ZERO(fd_set *set); set에 있는 fd를 전부 제거합니다.

 

이 select를 활용하여 파일 3개로부터 입력이 들어오면 출력해주는 일종의 서버 프로그램을 작성해보도록 합시다. 여기서 나오는 select와 poll은 정규파일에서 다루는 것은 적절하지 않습니다. 이러한 함수들은 정규 파일을 감시하게 되면 바로 준비되었다고 판단하여 return 되어 버리기 때문입니다. 정규 파일은 언제나 디스크에서 읽을 준비가 되어있습니다. 그래서 디스크 파일보다는 네트워크 입출력에서 다루는 것이 적절합니다.

여기서는 네트워크라는 개념은 배제하고 오직 상큼하게 select의 개념을 살펴보고 어떻게 다뤄보는지 느낌만 가져가 봅시다. 

예제

//multiIO_select.c 

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

#define FD_SIZE 3
#define BUF_SIZE 8

int main(int argc, char *argv[]){
        int i, n, ret, fd_count, end_count = 0;
        int fds[FD_SIZE];
        char buf[BUF_SIZE] = {0,};
        struct timeval tv;

        fd_set retfds, readfds;

        //3개의 파일의 이름을 받는다.
        if(argc != 4){
                printf("usage : %s file1 file2 file3\n", argv[0]);
                return 0;
        }

        //readfds를 공집합으로 만든다. 
        FD_ZERO(&readfds);

        for(i = 0; i < FD_SIZE; i++){
                fds[i] = open(argv[i+1],O_RDONLY);
                if(fds[i] >= 0){
                        //readfds 집합에 파일디스크립터 fds[i]를 추가한다. 
                        FD_SET(fds[i], &readfds);
                        //맨 마지막 파일열린 파일 디스크립터가 가장 큰 값을 갖는다.
                        fd_count = fds[i];
                }
        }

        while(1){

                //readfds를 retfds로 옮기는 이유는 select를 통해서 readfds가 바뀔 수 있다.
                retfds = readfds;

                //열린 파일 디스크립터의 최대값 +1
                ret = select(fd_count + 1, &retfds, NULL, NULL, NULL);

                if(ret == -1){
                        perror("select error ");
                        exit(0);
                }

                for(i = 0; i < FD_SIZE; i++){
                        //만약 fds[i]에 대해서 읽을 준비가 된 파일 디스크립터이면 
                        if(FD_ISSET(fds[i], &retfds)){
                                //읽고 출력해준다. 
                                while((n = read(fds[i], buf, BUF_SIZE)) > 0){

                                        //quit가 들어오면 fds[i] read 종료
                                        if(!strcmp(buf,"quit\n")){
                                                //readfds에서 지우고 
                                                FD_CLR(fds[i], &readfds);
                                                //파일디스크립터 close
                                                close(fds[i]);

                                                end_count++;
                                                if(end_count == FD_SIZE) 
                                                        exit(0);
                                                continue;
                                        }

                                        printf("fd[%d] - %s",fds[i], buf);
                                        memset(buf, 0x00, BUF_SIZE);
                                }
                        }
                }

        }
}

위의 코드는 아주 간단합니다.  3개의 파일에 대해서 읽을 준비가 된 파일 디스크립터가 있으면 해당 파일을 읽어서 출력해주는 프로그램입니다.

FD_ZERO를 이용해 readfds를 초기화 시킨 후 readfds에 감시할 파일 디스크립터를 추가합니다. 그것이 FD_SET입니다. readfds의 복사본인 retfds select 시스템콜을 통해서 감시를 시작합니다. 만약 복사본을 넣지 않고 쌩으로 readfds를 넣는다면 select함수는 readfds를 단순히 조회만 하지 않고 변경합니다. 이러한 변경은 불필요합니다. FD_ISSET을 통해서 현재 준비된 파일디스크립터를 처리한 후에 다시 변경되지 않은 readfds로 돌아가야합니다.

만약 retfds flag on이 되면 FD_ISSET true를 반환하게 됩니다. 그러면 파일에 업데이트 된 내용을 출력해주게 됩니다결론적으로 위 소스 코드는 파일을 읽었다고 끝내지 않고 계속 파일에 쓰인 내용을 출력해주는 프로그램입니다.

이를 시험해보기 위해서는 다른 프로그램이 필요합니다.

//fwrite.c

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

int main(int argc, char *argv[]){
        int fd, ret;
        char buffer[32];

        if(argc != 2){
                printf("usage : %s file \n", argv[0]);
                exit(0);
        }
        //파일을 쓰기 전용으로 연다.
        fd = open(argv[1], O_WRONLY|O_APPEND);

        if(fd < 0){
                perror("file erorr ");
                exit(0);
        }

        while(1){

                //한줄 처리를 위해서 fgets를 사용
                fgets(buffer, sizeof(buffer), stdin);

                //파일에 기록 
                ret = write(fd, buffer, strlen(buffer));

                if(ret < 0){
                        perror("write error ");
                        exit(0);
                }
                //quit문자열은 종료
                if(!strcmp(buffer, "quit\n")) break;
        }

        close(fd);
        return 0;
}

 

이 실행 파일은 단순히 argv[1]로 들어오는 파일명에 기록하는 프로그램입니다. 이제 두 소스 코드를 컴파일하고 쓰일 실행 파일을 생성합니다.

# gcc multiIO_select.c 
# gcc fwrite.c -o fwrite
# touch a b c

 

이제 터미널 총 4개를 띄어서 테스트해볼건데, 하나는 select함수를 사용하는 프로그램, 다른 3개 터미널에는 fwrite를 수행하는 프로그램으로 select를 실험합니다. 단, 같은 디렉토리에 있어야합니다.

./a.out a b c
# ./a.out a b c
fd[3] - writing fd[3] - a...
fd[4] - writing fd[4] - b...
fd[5] - writing fd[5] - c...
fd[3] - hello
fd[4] - world
fd[5] – bye
#
./fwrite a ./fwrite b ./fwrite c
# ./fwrite a
writing a...
hello
quit
#
# ./fwrite b
writing b...
world
quit
# 
# ./fwrite c
writing c...
bye
quit
#

위와 같이 출력이 더럽게 나오는 것은 BUF_SIZE가 작아서 그렇습니다. BUF_SIZE를 늘리게 되면 위와 같은 지저분한 출력을 예방할 수는 있습니다. 

MultiIO_select.c에서는 select를 이용해서 여러 파일 디스크립터의 내용들을 입력받아서 출력해주고 있습니다. 설명드렸듯이 select는 정규파일에 대해서 항상 준비되어있기 때문에 곧 장  return되어 버립니다. 티는 나지 않지만 multiIO_select.c의 select함수 호출 이후 printf로 아무 로그나 찍으면 엄청나게 루프를 도는 것을 확인할 수 있습니다. 여기서는 이해를 돕기 위해서 만든 프로그램일 뿐입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

재지정(Redirection)

리눅스에서 프로그램은 보통 세 개의 파일 서술사를 열게 됩니다. 바로 표준 입력(standard input, STDIN), 표준 출력(standard output, STDOUT), 그리고 표준 에러(standard error, STDERR)가 되지요. 

순서대로 파일 디스크립터(fild descriptor)는 0, 1, 2입니다.

 

리다이렉션(Redirection)이라고 하는 것은 이러한 서술자 중 하나 이상을 파일로 다시 지정하는 수단이라고 볼 수 있는데요. 어떻게 리다이렉션을 사용할까요?

 

1) program > file_name

>는 이해하기 쉽습니다. program에서 출력하는 것을 file_name이라는 파일에 기록하겠다는 것입니다. 더 잘 이해하기 위해서 저의 리눅스를 활용해보도록 하지요.

제 리눅스의 /tmp 디렉터리에는 다음과 같은 파일들이 있습니다.

무슨 파일들인지는 잘 모르겠네요.


[root@localhost tmp]# ls
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-ModemManager.service-OOhWCc
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-bolt.service-KuASd9
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-chronyd.service-mPGGCi
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-colord.service-uTlHDZ
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-fwupd.service-mV4Wkl
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-rtkit-daemon.service-eaiBmp

ls 명령을 치는 순간 화면에 보이는 파일들의 목록이 있죠? 이것은 표준 출력으로 표시한 것입니다. 쉽게 이해하기 위해서 모니터가 표준 출력이라고 보세요.

 

이제는 이것을 파일로 저장해보도록 합시다. 위의 꺽쇠를 이용하면 됩니다.


[root@localhost tmp]# ls > list_stdout
[root@localhost tmp]# cat list_stdout
list_stdout
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-ModemManager.service-OOhWCc
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-bolt.service-KuASd9
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-chronyd.service-mPGGCi
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-colord.service-uTlHDZ
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-fwupd.service-mV4Wkl
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-rtkit-daemon.service-eaiBmp
tracker-extract-files.0

그러면 list_stdout이 생겨날텐데 이것을 cat명령어로 실행해보면 파일의 내용을 볼 수 있습니다. 위의 ls 명령어의 결과와 같죠?

 

그렇다면 다음의 명령은 어떻게 출력이 될까요? 분명 위의 ls에는 aaa.txt라는 파일은 없었습니다. 과연 파일로 저장될까요? 


[root@localhost tmp]# ls -l aaa.txt > list_aaa
ls: cannot access 'aaa.txt': 그런 파일이나 디렉터리가 없습니다
[root@localhost tmp]# cat list_aaa

list_aaa가 생겨나지 않았고 메시지가 출력이 되네요. 여기서 위의 메시지는 stderr로 출력된 에러 메시지입니다. stderr로 출력된 에러 메시지는 list_aaa의 파일로 저장이 되지 않네요. 

 

그 이유는 기본적으로 ls -l aaa.txt > list_aaa는 ls -l aaa.txt 1> list_aaa와 같이 동작합니다. 여기서 숫자 1은 표줄 출력(stdout)을 의미하게 됩니다. 1이 파일 디스크립터라면 우리는 표준 에러는 다음과 같이 저장할 수 있겠군요. 

 

아래의 결과를 보면 2(표준에러)가 리다이렉션되어 파일의 내용으로 전달되는 것을 알 수 있습니다.

 


[root@localhost tmp]# ls -l aaa.txt 2> list_aaa
[root@localhost tmp]# cat list_aaa
ls: cannot access 'aaa.txt': 그런 파일이나 디렉터리가 없습니다

 

2) program >> file_name

꺽쇠가 두개 연달아 있는 것은 파일의 끝에 내용을 덧붙이라는 것으로 여러분이 직접 실험해보시기 바랍니다.

 

 

 

 

3) program < file_name

여기서부터는 조금 헷갈릴 수 있으니 잘 들으세요. 아니, 보세요. file_name의 파일 내용을 program의 표준 입력으로 사용하겠다 라는 의미가 됩니다. 

 

ls는 표준입력을 받지 않는다.

자, 프로그램에는 표준 입력으로 사용자의 입력을 받는 것이 있고 그렇지 않는 프로그램이 있습니다. 여기서 ls는 사용자의 입력을 표준입력으로 받을까요? 결론은 아닌데요. 

아래의 내용으로 확인해보겠습니다.

우선 아까 없던 aaa.txt를 만들어볼텐데 aaa.txt에는 aaa라는 문자를 기록하도록 하지요. 그리고 다시 echo를 사용해 bbb라는 내용을 갖는 aaa파일을 만들어봅시다.


[root@localhost tmp]# echo aaa > aaa.txt
[root@localhost tmp]# ls aaa.txt
aaa.txt
[root@localhost tmp]# echo bbb > aaa
[root@localhost tmp]# ls aaa
aaa

(echo는 그 다음의 입력을 표준 출력으로 출력하는데 아까 배운 >을 통해서 파일로 저장했습니다. )

 

이제 ls에 aaa.txt의 내용을 표준입력으로 받아보도록 하겠습니다. 


[root@localhost tmp]# ls < aaa.txt
aaa
aaa.txt
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-ModemManager.service-OOhWCc
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-bolt.service-KuASd9
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-chronyd.service-mPGGCi
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-colord.service-uTlHDZ
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-fwupd.service-mV4Wkl
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-rtkit-daemon.service-eaiBmp
tracker-extract-files.0

뭐 얼핏보면 뭔가 출력이 됐는데 우리가 원하는 결과는 aaa.txt의 내용은 bbb가 ls의 입력으로 들어가서 ls aaa라는 명령어가 수행되어야하는 것입니다. 근데 제가 전달하는 입력을 씹고 현재 디렉터리의 파일 내용을 출력하고 있죠. 이것은 ls의 기본 동작입니다. 

ls는 이 처럼 표준 입력을 받지 않습니다. 대신 argv라고하는 argument vector를 통해서 사용자 입력을 받고 있는 셈입니다.

 

표준입력을 받는 명령어는 무엇이 있을까요? 

대표적으로 cat, grep 등이 있습니다. 

 

cat의 기본 동작

cat의 기본동작(아무런 옵션을 주지 않을때의 동작)은 입력받은 거 그대로 출력하는 기능입니다. 그 입력은 표준입력으로 받습니다. 

표준 입력으로 받으니 이것을 파일의 내용을 입력으로 줘보겠습니다.


[root@localhost tmp]# echo Hello > hello.txt
[root@localhost tmp]# cat < hello.txt
Hello

 

cat명령어는 argv로 파일 이름을 인자로 받을 수 있도록 프로그램이 되어있는데, 결과는 위와 같습니다.

 

 

 

 

grep의 기본 동작

grep은 기본적으로 바로 뒤에 인자가 포함된 문자열이 입력이 되면 그 문자열을 다시 출력해줍니다. 아래의 결과를 보면 알 수 있는데 이 grep은 표준 입력으로 입력을 받습니다.


[root@localhost tmp]# grep reak
AAA
BBB
rea
rreak
rreak
rreakk
rreakk
mmsmsms

자, 여기서 눈 여겨 봐야할 것은 grep은 굵은 글씨인 rreak, rreakk만 출력하였습니다. 왜냐면 인자로 받은 reak라는 문자열이 포함되있기 때문이죠.

 

표준 입력으로 받으니 우리는 파일을 입력으로 리다이렉션을 쓸 수 있겠군요. 위의 입력했던 내용을 그대로 vi 편집기로 저장합니다. 


[root@localhost tmp]# vi grep_test
[root@localhost tmp]# grep reak < grep_test
rreak
rreakk


vi 편집기 내용


AAA 
BBB 
rea 
rreak
rreakk
mmsmsms 

 

표준 입력으로 받은 결과와 동일함을 알 수 있네요.

 

리다이렉션을 통한 Copy

리다이렉션을 통해서 파일을 복사할 수도 있는데요. 아래를 보도록 합시다.  echo로 file을 생성한 후에 cat의 입력으로 file의 내용을 받습니다. 그 후에 cat은 표준 출력으로 file의 내용을 출력하는데 다시 >로 표준 출력의 내용을 file_copied라는 파일에 저장합니다. 결국은 복사를 수행한 것과 같게 됩니다.


[root@localhost tmp]# echo hello > file
[root@localhost tmp]# cat < file > file_copied
[root@localhost tmp]# cat file_copied
hello

 

 

 

 

 

파이프( | )

파이프라는 것은 program1의 표준 출력을 program2의 표준입력으로 입력받습니다. 명령어의 결과를 다시 어떤 명령어의 입력으로 받아야할때 사용되며 리눅스에서 매우 흔히 쓰입니다. 형식은 아래와 같습니다.

program1 | program2

파이프라는 것은 program1의 표준 출력을 program2의 표준입력으로 입력받습니다. 리눅스에서 매우 흔히 쓰이는데요. 가장 빈번하게 사용하는 grep과 연관시켜서 사용해봅시다. 

우선 제 리눅스에서 /bin으로 이동했습니다. 그리고 ls -l 명령을 보면 무수히 많은 파일들이 보이는데 그 중에서 zip이라는 문자열을 포함한 것을 확인해보고 싶습니다. 

그렇다면 ls -l로 우선 표준 출력을 하고 이 표준 출력된 내용을 zip이라는 인자를 전달받는 grep에게 표준 입력으로  전달시키면 되겠죠?

답은 간단합니다. 아래의 명령을 실행시켜보세요.


[root@localhost bin]# ls -l | grep zip
lrwxrwxrwx. 1 root root           5  5월 10  2019 bunzip2 -> bzip2
lrwxrwxrwx. 1 root root           5  5월 10  2019 bzcat -> bzip2
-rwxr-xr-x. 1 root root       38472  5월 10  2019 bzip2
-rwxr-xr-x. 1 root root       17560  5월 10  2019 bzip2recover
-rwxr-xr-x. 1 root root       40256  5월 10  2019 funzip
-rwxr-xr-x. 1 root root        3447  5월 10  2019 gpg-zip
-rwxr-xr-x. 1 root root        2345 11월  8  2019 gunzip
-rwxr-xr-x. 1 root root      170544 11월  8  2019 gzip
lrwxrwxrwx. 1 root root           6  5월 11  2019 mzip -> mtools
-rwxr-xr-x. 1 root root        5656  5월 13  2019 preunzip
-rwxr-xr-x. 1 root root        5656  5월 13  2019 prezip
-rwxr-xr-x. 1 root root       13352  5월 13  2019 prezip-bin
-rwxr-xr-x. 2 root root      210408  5월 10  2019 unzip
-rwxr-xr-x. 1 root root      105064  5월 10  2019 unzipsfx
-rwxr-xr-x. 1 root root      234496  5월 11  2019 zip
-rwxr-xr-x. 1 root root      105376  5월 11  2019 zipcloak
-rwxr-xr-x. 1 root root        2953 10월 10  2008 zipgrep
-rwxr-xr-x. 2 root root      210408  5월 10  2019 zipinfo
-rwxr-xr-x. 1 root root      100104  5월 11  2019 zipnote
-rwxr-xr-x. 1 root root      100112  5월 11  2019 zipsplit

굳이 그림으로 표현하면 아래와 같이 실행됩니다.

 

이렇게 리다이렉션과 파이프에 대해서 알아보았습니다.

궁금하신 점은 댓글로 달아주세요.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

fcntl

파일을 열었는데 그 속성을 바꾸려면 어떻게 할까요? fcntl함수는 이미 열린 파일의 속성들을 변경할 수 있습니다.

#include <unistd.h> 
#include <fcntl.h> 
int fcntl(int fd, int cmd, ... /* arg */ );

 

fd : 파일의 속성을 조회하거나 변경할 파일 디스크립터입니다. 

cmd : 커맨드입니다. cmd 종류는 아래와 같습니다.

cmd 설명
F_GETFL fd에 대한 파일 상태 속성들을 반환값으로 돌려줍니다. O_RDONLY, O_WRONLY, O_RDWR, O_EXEC, O_SEARCH는 개별적으로 판정할 수 없고 ACCMODE라는 마스크를 이용하여 알아낼 수 있습니다. 이는 아래의 예제를 통해서 알아보도록 하지요.
F_SETFL 파일 상태 속성들을 세번째 인수로 받아 설정합니다. O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC 속성만 변경할 수 있습니다. 읽기, 쓰기 관련 플래그(O_WRONLY, O_RDONLY, O_RDWR)은 조회할 수는 있지만 변경할 수 없음을 유의하세요.
F_GETOWN 현재 SIGIO, SIGURG 신호를 받도록 설정된 프로세스 ID 혹은 프로세스 그룹 ID를 돌려줍니다. 
비동기 입출력과 관련이 있습니다.
F_SETOWN SIGIO와 SIGURG신호를 받도록 시정된 프로세스 ID나 프로세스 그룹 ID를 설정합니다.
F_DUPFD 파일 서술자 fd를 복제합니다. 새 파일 서술자를 함수의 반환값으로 돌려줍니다. 이때 반환된 새 서술자는 FD_CLOEXEC속성이 해제된 상태입니다. 그렇기 때문에 exec류의 함수로 다른 명령을 실행했을때 열린 상태로 남아있습니다.
F_DUPFD_CLOEXEC 파일 서술자를 복제하고 새 파일 서술자에 관련된 FD_CLOEXEC를 설정합니다. 역시 새 파일 서술자가 반환됩니다.
F_GETFD fd에 대한 파일 서술자 플래그들을 반환합니다. 정의되어 있는 파일 서술자 flag는 FD_CLOEXEC입니다.
F_SETFD 서술자 플래그들을 설정합니다. 셋째 인자에 그 플래그를 전달합니다.

위 함수는 cmd에 따라 반환값이 다르고 실패시 -1을 반환하게 됩니다.

 

속성 조회, 변경 예제

아래는 파일의 속성을 조회해보고 O_APPEND 속성을 추가하여 다시 조회하는 예제입니다. 

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

void printAccessMode(int flags){

        switch(flags & O_ACCMODE){
        case O_RDONLY:
                        printf("read only\n");
                        break;

        case O_WRONLY:
                        printf("write only\n");
                        break;
        case O_RDWR:
                        printf("read write\n");
                        break;
        }

        printf("append %s \n", (flags & O_APPEND) ? "O":"X");
        printf("nonblocking %s \n", (flags & O_NONBLOCK) ? "O":"X");
        printf("\n");
}

int main(){
        int fd = open("test.txt", O_RDONLY|O_CREAT, 0666);

        int flags = fcntl(fd, F_GETFL,0);
        printAccessMode(flags);

        flags |= O_APPEND;

        fcntl(fd, F_SETFL, flags);

        flags = fcntl(fd, F_GETFL, 0);
        printAccessMode(flags);

}

 

주의해야할 점은 파일 서술자 플래그, 파일 상태 플래그를 수정할때는 반드시 먼저 플래그를 얻어오고(GET) 얻어온 값을 통해 변경(SET)해야합니다. 그렇지 않고 바로 SET하면 이전에 설정했던 값은 지워질 수 있습니다. 

 

결과

# ./a.out
read only
append X
nonblocking X

read only
append O
nonblocking X

 

Non-blocking 예제

표준 입력(fd 0)으로 입력을 받는데, 이것을 non-blocking으로 하고 싶을때 fcntl을 통해서 구현할 수 있습니다. 바로 O_NONBLOCK을 통해서 실현할 수 있죠.

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


int main(){
        char buf[128] = {0,};
        int fd = 0;
        int flags = fcntl(fd, F_GETFL,0);

        flags |= O_NONBLOCK;
        //fcntl(fd, F_SETFL, flags);

        read(fd, buf, sizeof(buf));
        printf("buf : %s \n", buf);
}

 

위의 코드를 그대로 실행하면 우리가 알고 있듯, 입력을 받을때까지 프로그램은 대기하게 됩니다. 사용자가 입력을 하게 되면 그때 입력 내용을 출력하고 프로그램이 끝이 납니다.

# ./a.out
Hello!!
buf : Hello!!

 

이제 주석을 해제하면 사용자 입력을 기다리지 않고 바로 버퍼를 출력하고 끝납니다. 

# ./a.out
buf :

 

이것을 조금만 응용하면 사용자가 입력할 시간을 주고 제한 시간내에 입력이 없으면 그에 따른 로직을 도는 프로그램을 짤 수도 있을 겁니다.

 

CLOEXEC 예제

어떤 파일을 열어서 fd를 얻었고 exec류의 함수로 다른 프로그램을 실행하려고 합니다. 이때 다른 프로그램은 이미 열려진 fd를 사용하지 않음에도 fd를 그대로 가져가게 되죠. 다음의 코드가 그런 현상을 보여줍니다. 

 

파일을 오픈한 후 execl함수로 loop이라는 프로그램을 실행합니다.

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

int main(){
        int fd = open("test.txt",O_RDONLY|O_CREAT,0666);
        int flags = fcntl(fd, F_GETFD, 0);
        flags |= FD_CLOEXEC;

        //fcntl(fd, F_SETFD, flags);

        execl("/root/examples/file_test/loop","./loop",0);
}

 

loop프로그램은 다음의 코드로 만들어졌습니다. pid출력하고 무한루프를 돕니다. 별거없죠?

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

int main(){
        printf("loop start pid:%d\n",getpid());
        while(1);
}

 

두개를 컴파일하고 무한 루프 프로그램말고 execl을 호출하는 프로그램을 실행해보도록 하지요.

# ./a.out &
[2] 6808
# loop start pid:6808

 

 

이제 해당 pid의 fd를 보시기 바랍니다. 그러면 loop 프로그램에서 열린 fd를 볼 수 있는데, 맨 아래의 test.txt도 열려있음을 확인할 수 있습니다.

# ls -l /proc/6808/fd
합계 0
lrwx------. 1 root root 64  6월 27 18:54 0 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:54 1 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:54 2 -> /dev/pts/1
lr-x------. 1 root root 64  6월 27 18:54 3 -> /root/examples/file_test/test.txt

 

이 fd를 exec시에 닫고 싶다면 위의 주석을 해제하고 다시 실행해보세요.

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

int main(){
        int fd = open("test.txt",O_RDONLY|O_CREAT,0666);
        int flags = fcntl(fd, F_GETFD, 0);
        flags |= FD_CLOEXEC;

        fcntl(fd, F_SETFD, flags);

        execl("/root/examples/file_test/loop","./loop",0);
}

 

그러면 아래와 같이 test.txt가 loop 프로세스에서 닫혀져있음을 알 수 있습니다.

# ./a.out &
[3] 6893
# loop start pid:6893

#
# ls -l /proc/6893/fd
합계 0
lrwx------. 1 root root 64  6월 27 18:57 0 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:57 1 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:57 2 -> /dev/pts/1

 

지금까지 fcntl 설명과 fcntl을 이용해서 파일의 속성을 바꾸는 예제를 보았습니다. 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

디스크 여유 공간과 디스크 사용량을 보기 위해서 리눅스에서는 df, du가 있습니다.

df(Disk Free)

시스템 전체에 마운트 된 디스크의 여유 공간을 출력해줍니다.

파일시스템, 디스크 크기, 사용된 용량, 사용가능한 용량, 사용된 용량의 비율, 마운트된 지점을 순서대로 출력해줍니다.
아무 옵션없이 이렇게 보면 알기가 좀 까다롭죠?

-h Human readable로 사람이 읽기 좋게 출력해줍니다.

-k 위 옵션은 키로바이트 단위로 출력해줍니다.

-i 사용가능한 또는 사용중인 inode에 대한 정보를 출력해줍니다.

 

du(Disk Usage)

디스크 사용량을 알기위한 명령어인데, 뒤 인자에 경로를 붙이지 않는다면 현재 디렉토리에서부터 각 디렉토리의 디스크 사용량을 출력합니다.

 

-h df에서와 같이 사람이 읽기좋게 출력해줍니다.

 

-s 디렉토리를 표시하지 않고 단지 사용량만 알고 싶다면 Summary의 약자인 s옵션을 쓰면 됩니다. 읽기 좋게 h와 같이 써보세요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

getopt로 명령인자 분리

 

ls -il hello.txt

위와 같은 명령을 구현하고자 할때 명령어에 대한 옵션 분리가 필요합니다. -i 옵션과 -l 옵션이 있고, 위의 명령어에서 hello.txt라는 파일 이름을 줄 수도 있고 안줄 수도 있습니다. 

getopt를 모른다면 이 명령 옵션 인자를 스스로 파싱해서 써야할건데 자신있으신가요? 저는 자신이 없어서 getopt를 설명하고자 합니다.

 

getopt를 사용하기 위해서는 unistd.h 헤더파일을 추가해야합니다.


#include
<unistd.h>

int getopt(int argc, char* const argv[], const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

getopt는 명령어를 argv에 있는 명령어를 계속적으로 parsing하는데, 정상적으로 파싱이 되면 optstring에서 지정한 문자열이 반환되고 파싱이 전부되었다면 최종적으로 -1을 반환합니다. 

 

어려운거는 별로 없으니 바로 예제를 보도록 하지요.

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

void showHelp(const char* thisName){
        printf("usage : %s [OPTION] [MESSAGE]\n"
                        " -h            help\n"
                        " -w [MESSAGE]  write file\n"
                    " -r                read file\n"
                        , thisName);
}

void writeFile(const char* message){
        int fd = open("getopt_file.txt",O_RDWR|O_CREAT,0777);
        write(fd, message, strlen(message));
        close(fd);
}

void readFile(){
        char buf[32]={0,};
        int n;
        int fd = open("getopt_file.txt",O_RDWR|O_CREAT,0777);
        while((n = read(fd, buf, sizeof(buf))) > 0)
                printf("%s",buf);

        close(fd);
}
int main(int argc, char* argv[]){
                char opt;
                opterr = 0;

                if(argc < 2){
                        showHelp(argv[0]);
                        exit(0);
                }

                while((opt = getopt(argc, argv, "hw:r")) != -1){
                        printf("opt:%c, optopt:%d\n", opt, optopt);
                        switch(opt){
                        case 'h':
                                showHelp(argv[0]);
                                break;
                        case 'w':
                                writeFile(optarg);
                                break;

                        case 'r':
                                readFile();
                                break;

                        }
                }
}

 

h는 help 옵션, w는 파일에 write하는 명령 옵션, r은 파일을 읽는데 사용하는 명령 옵션입니다. 

다른건 이해가 가실 것이고 optstring을 보면 w옆에 :이 있네요. w는 다음에는 MESSAGE라는 문자열이 오게 되지요. 이처럼 명령어 옵션 이후에 인자가 필요할때 그 문자 다음에 :을 사용하는데요. 명령 다음 인자는 optarg에 저장이 됩니다.

 

 

컴파일 후 실행하면서 어떻게 실행되는지 보겠습니다.


# gcc getopt_test.c -o getopt_test
# ./getopt_test
usage : ./getopt_test [OPTION] [MESSAGE]
 -h             help
 -w [MESSAGE]   write file
 -r             read file
# ./getopt_test -h
opt:h, optopt:0
usage : ./getopt_test [OPTION] [MESSAGE]
 -h             help
 -w [MESSAGE]   write file
 -r             read file
# ./getopt_test -w "hello"
opt:w, optopt:0
# ./getopt_test -r
opt:r, optopt:0
hello

만약 hwr이외에 옵션을 주면 어떻게 될까요?

 


# ./getopt_test -k
opt:?, optopt:107

알 수 없는 옵션을 주게 되거나 옵션 이후 인자가 필요한데 주지 않으면 ?을 반환하는 것을 알 수 있습니다.

 

이제 optopt, opterr을 보도록 하지요.

optopt는 getopt가 제대로 실행이 되었다면 0이 되는데, 그렇지 않을 경우 그 문자가 들어갑니다.

opterr은 에러 발생시 출력할지 말지를 정합니다. opterr을 1로 해놓으면 getopt에서 에러메시지를 출력하는데 0을 지정하면 출력하지 않습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

wait, waitpid 함수는 아래의 header파일을 include해야합니다. 이 두 함수는 부모프로세스가 자식프로세스를 기다리는 공통점이 있습니다만 waitpid는 어떤 자식을 기다릴지 조금 더 구체적으로 지정할 수 있습니다.

이 두 함수의 반환값은 성공시 process id를 반환하고 오류시 -1을 반환합니다. 

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

 

wait 함수

pid_t wait(int *wstatus);

 

성공시 프로세스의 pid를 반환합니다. wstatus는 종료상태를 알 수 있는데, 굳이 종료상태를 알 필요가 없다면 NULL을 전달해주세요.

wstatus를 알 수 있는 방법은 <sys/wait.h>에 정의되어있는 매크로들을 사용하면 됩니다. 아래는 그 매크로들과 설명입니다. 

매크로 설명
WIFEXITED(wstatus) returns  true  if  the  child  terminated normally, that is, by calling exit(3) or
              _exit(2), or by returning from main().
WEXITSTATUS(wstatus) returns the exit status of the child.  This consists of the  least  significant  8
              bits  of  the  status  argument  that  the child specified in a call to exit(3) or
              _exit(2) or as the argument for a return statement in main().  This  macro  should
              be employed only if WIFEXITED returned true.
WIFSIGNALED(wstatus) returns true if the child process was terminated by a signal.
WTERMSIG(wstatus) returns the number of the signal that caused the child process to terminate.  This
              macro should be employed only if WIFSIGNALED returned true.
 WCOREDUMP(wstatus) returns true if the child produced a core dump.  This  macro  should  be  employed
              only if WIFSIGNALED returned true.

              This  macro  is  not  specified  in POSIX.1-2001 and is not available on some UNIX
              implementations (e.g., AIX, SunOS).  Therefore,  enclose  its  use  inside  #ifdef
              WCOREDUMP ... #endif.
WIFSTOPPED(wstatus) returns  true  if  the  child process was stopped by delivery of a signal; this is
              possible only if the call was done using WUNTRACED or  when  the  child  is  being
              traced (see ptrace(2)).
WSTOPSIG(wstatus) returns  the  number  of  the  signal  which caused the child to stop.  This macro
              should be employed only if WIFSTOPPED returned true.
 WIFCONTINUED(wstatus) (since Linux 2.6.10) returns true if the child process was resumed by delivery  of
              SIGCONT

 

그렇다면 이제 예제를 보도록 하겠습니다.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
        int status, cpid, endpid;

        if((cpid=fork())==0){
                printf("\tchild process:%d\n",getpid());
                sleep(10);
                printf("\tchild end\n");
                exit(10);
        }

        endpid=wait(&status);
        printf("end pid : %d\n",endpid);
        printf("WIFEXITED : %d\n",WIFEXITED(status));
        printf("WEXITSTATUS : %d\n",WEXITSTATUS(status));
        printf("WIFSIGNALED : %d\n",WIFSIGNALED(status));

        printf("\n");
        printf("parent end\n");

}

 

이후 아래와 같이 컴파일하고 정상적인 결과를 보도록 해봅시다.

# gcc process_wait.c
# ./a.out
        child process:5031
        child end
end pid : 5031
WIFEXITED : 1
WEXITSTATUS : 10
WIFSIGNALED : 0

parent end

 

EXITSTATUS는 10임을 알 수 있는데 이는 위 코드에서 exit(10)을 사용했기 때문입니다. 시그널을 받지 않고 정상적인 종료를 한 케이스는 이렇게 나오는군요.

이제 background로 프로세스를 돌리고 kill해서 시그널을 보내보도록 하겠습니다.

# ./a.out &
[4] 5060
#     child process:5061

# kill 5061
# end pid : 5061
WIFEXITED : 0
WEXITSTATUS : 0
WIFSIGNALED : 1

parent end

[4]+  Done                    ./a.out

 

그렇다면 WIFEXITED는 false이고 WIFSIGNALED는 true네요. 이처럼 위의 매크로로 자식 프로세스의 종료 상태를 알 수 있습니다.

그런데 wait은 종료되는 자식 프로세스 중 먼저 종료되는 프로세스가 있다면 바로 호출되어지는데 만약 우리가 특정 자식프로세스만을 기다린다할때는 아래의 waitpid함수를 사용해야합니다.

 

waitpid

pid_t waitpid(pid_t pid, int *wstatus, int options);

역시 호출 성공시 그 프로세스의 pid, 아니라면 -1을 반환합니다. 

pid: 인자로 pid는 기다릴 프로세스 식별자인데 양수일 경우만 해당합니다. 아래는 pid에 따라서 어떻게 동작하는지를 설명합니다.

 

pid 설명
< -1  meaning wait for any child process whose process group ID is equal to the absolute value of pid. 
 -1 meaning wait for any child process. 
0 meaning wait for any child process whose process group ID is equal to that of  the calling process.
> 0  meaning wait for the child whose process ID is equal to the value of pid. 

 

wstatus : wstatus는 위의 wait과 같이 종지상태를 얻어옵니다.

option : option은 조금 더 구체적인 동작을 지정합니다. option의 종류는 아래와 같습니다.

 

OPTION 설명
WNOHANG return immediately if no child has exited. 
WUNTRACED also  return  if  a child has stopped (but not traced via ptrace(2)).  Status for traced children which have stopped is provided even if this option is not specified. 
WCONTINUED (since Linux 2.6.10) also return if a stopped child has been resumed by delivery of SIGCONT. 

 

wait(&status)은 waitpid(-1, &wstatus, 0)를 호출한 것과 같습니다.

 

waitpid 예제

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
        int state, cpid1, cpid2, endpid;

        if((cpid1=fork())==0){
                printf("\tchild1 process:%d\n",getpid());
                sleep(2);
                printf("\tchild1 end\n");
                exit(0);
        }

        if((cpid2=fork())==0){
                printf("\tchild2 process:%d\n",getpid());
                sleep(4);
                printf("\tchild2 end\n");
                exit(0);
        }

        endpid=waitpid(cpid1,&state,0);
        printf("pid : %d\n",endpid);

        endpid=waitpid(cpid2,&state,0);
        printf("pid : %d\n",endpid);


        printf("parent end\n");

}

 

여기서 자식 프로세스를 2개 생성하고 기다리는데 waitpid로 첫번째 자식 프로세스, 두번째 자식 프로세스를 기다립니다. 만약 여기서 두 번째 자식 프로세스를 기다리고 싶지 않다면 두 번째 waitpid를 하지 않으면 되겠죠.

결과는 아래와 같습니다.

#gcc process_waitpid.c
# ./a.out
        child2 process:5614
        child1 process:5613
        child1 end
pid : 5613
        child2 end
pid : 5614
parent end

 

 

만약 두번째 프로세스가 끝났을때는 그대로 종료하지만 작업이 끝나지 않았을때는 A라는 작업을 해야하는 상황이라면 어떻게 해야할까요? 

wait은 계속 자식 프로세스가 끝날때까지 block되었는데 waitpid는 옵션을 사용하면 됩니다. 

 

아래가 그 예제입니다.

 

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
        int state, cpid1, cpid2, endpid;

        if((cpid1=fork())==0){
                printf("\tchild1 process:%d\n",getpid());
                sleep(2);
                printf("\tchild1 end\n");
                exit(0);
        }

        if((cpid2=fork())==0){
                printf("\tchild2 process:%d\n",getpid());
                sleep(4);
                printf("\tchild2 end\n");
                exit(0);
        }

        while(1){
                endpid=waitpid(cpid2,&state,WNOHANG);
                if(endpid==0){  //child not finished if pid = 0
                        printf("child2 not finished\n");
                        sleep(1);
                }else{
                        break;
                }

        }

        printf("parent end\n");

}

 

위 코드에서는 첫번째 자식 프로세스는 기다리지 않습니다. waitpid에 WNOHANG 옵션을 주어서 자식이 종료되었는지 아닌지에 따라 작업을 구분지을 수 있습니다. 이때 waitpid는 지정된 프로세스가 끝나지 않았다면 0을 반환하고 끝났다면 그 pid를 반환하게 됩니다.

결과는 아래와 같습니다.

# gcc process_waitpid2.c
# ./a.out
child2 not finished
        child2 process:5670
        child1 process:5669
child2 not finished
        child1 end
child2 not finished
child2 not finished
        child2 end
parent end

 

waitpid가 wait에서 제공하지 못하는 3가지를 제공합니다.

1. waitpid함수로 특정 pid를 기다릴 수 있습니다. wait은 임의의 자식에 대해서 종료상태를 알려주지요.

2. waitpid는 wait과는 달리 자식이 종료될때까지 기다리지 않아도 됩니다. 바로 호출을 할 수 있습니다.

3. WUNTRACED와 WCONTINUED 옵션을 통해서 작업 제어를 할 수 있습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

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

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

 

dup

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

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

 

dup2 

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

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

 

dup 예제

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

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

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


        fd2=dup(fd1);

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

}

 

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

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

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

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

 

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

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

 

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

 

dup2 예제

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

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

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

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

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

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

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

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

        close(fd1);

}

 

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

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

 

 

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

 

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

 

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

 

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

 

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

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

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

 

감사합니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

조건 변수

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

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

Thread1 Thread2

data값 +1 증가

값을 출력


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

 

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

 

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

 

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

 

pthread_cond_init

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

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

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

 

pthread_cond_wait

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

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

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

 

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

 

pthread_cond_signal

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

int pthread_cond_signal(pthread_cond_t *cond);

 

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

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

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

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

 

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

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

pthread_mutex_t mutex;
pthread_cond_t cond;

int data=0;

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

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

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

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

    return 0;
}

 

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

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

 

생산자-소비자 예제

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

 

코드 설명:

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

 

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

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

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

 

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

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

 

코드는 아래와 같습니다.

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

#define Q_MAX       15 + 1

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

        pthread_mutex_t mutex;
        pthread_cond_t  c_cond;
        pthread_cond_t  p_cond;

}queue_t;

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

};

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

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

void put( int d ){

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

int get(){

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

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

void * produce(void *arg){

        int i=0;
        while(1){

                pthread_mutex_lock(&queue.mutex);

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

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

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

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

                if(i==100) break;

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

}



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

                pthread_mutex_lock(&queue.mutex);

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

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

                pthread_mutex_unlock(&queue.mutex);

                usleep(rand()%1000);
        }

        return 0;

}



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

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

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

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

        return 0;

}

 

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

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


...


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

 

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

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

임계 영역(Critical Section) 

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

 

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

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

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

 

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

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

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

 

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

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

 

뮤텍스(MutEx)

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

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

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

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

int cnt=0;

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

int main()
{
    pthread_t thread1,thread2;

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

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

 

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

 

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

 

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

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

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

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

 

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

 

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

 

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

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

 

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

int pthread_mutex_destroy(pthread_mutex_t *mutex);

 

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

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

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

pthread_mutex_t mutex;
int cnt=0;

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

    pthread_mutex_lock(&mutex);

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

int main()
{
    pthread_t thread1,thread2;

    pthread_mutex_init(&mutex,NULL);

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

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

    pthread_mutex_destroy(&mutex);
}

 

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

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

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

 

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

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,