[Raspberry Pi] 라즈베리 파이 사용하기 ②

예빈·2025년 12월 29일

Embedded/Linux

목록 보기
13/21


0️⃣ 들어가며

리눅스는 모든 자원을 파일처럼 관리하는 것이 특징이다.
이번 글에서는 리눅스 프로그래밍의 기본 개념과 파일 입출력에 관해 다룬다.
이전에 했던 내용과 중복되는 개념도 있지만 그만큼 잘 알아두면 도움이 될 것이다.


1️⃣ 학습 내용

4. 리눅스 프로그래밍의 기초

4.1 리눅스의 기본 구조와 파일 시스템

✅ 리눅스 시스템 프로그래밍

  • 리눅스 시스템 프로그래밍의 정의

    리눅스 운영체제(OS)가 제공하는 시스템 콜과 POSIX API를 사용해 user space에서 시스템 자원을 직접 제어하는 프로그래밍

    시스템 콜 : 사용자 프로그램이 커널 기능(파일 열기, 프로세스 생성 등)을 요청하는 인터페이스

    사용자 공간에서 커널 자원 접근 → open/read/writefork/execsocket 등

  • 학습 환경

    환경OS용도
    LSP @ Ubuntu 20.04 @ Vbox @ WindowsUbuntu (x86_64)가상화로 데스크톱에서 리눅스 프로그래밍 연습
    LSP @ Raspbian @ RPiRaspbian (ARM)라즈베리파이 실물 하드웨어 + GPIO 제어 실습
  • 리눅스 디바이스 프로그래밍

    구분리눅스 시스템 프로그래밍 (LSP)리눅스 디바이스 프로그래밍
    위치사용자 공간 (User Space)커널 공간 (Kernel Space)
    목적OS API로 시스템 자원 제어새 디바이스 드라이버 개발
    예제open("/sys/class/gpio/export")GPIO 드라이버 module_init()
    컴파일gcc app.c -o appmake modules + insmod
    실행sudo ./appdmesg로 커널 로그 확인
    대표 APIfork, socket, pthreadplatform_driver, file_operations

✅ 임베디드 리눅스

  • 아키텍처의 차이

    ARM/PPC(임베디드, 라즈베리파이) 등 PC가 아닌 곳에서 Linux를 사용하는 것을 일반적으로 임베디드 리눅스라 지칭함

  • 임베디드 리눅스 필요 요소

    • Bootloader (U-Boot)

      전원이 켜지면 가장 먼저 실행되는 코드

      하드웨어 초기화(GPU 끌기, 메모리 설정) 후 커널 로드

    • 커널 이미지

      make → vmlinux (실행파일, 10MB+ 매우 큼)

      make Image → 압축된 커널 (vmlinux 압축)

      make zImage → ARM용 최종 압축 이미지 (라즈베리파이 부팅용, kernel.org에서 다운로드 가능)

    • Device Tree

      CPU가 하드웨어의 메모리, GPIO, UART 위치를 알 수 있도록 바이너리 파일, 커널과 함께 부팅

    • Root Filesystem(ext4)

      / (루트) 디렉터리를 사용하는 파일 시스템

  • Mount

    커널이 파일시스템을 메모리에 올리고 / 경로로 연결하는 과정

    라즈베리파이에서는 SD카드의 2번째 파티션(ext4)을 /로 마운트

  • System Call

    사용자 프로그램(Process)이 파일이나 네트워크 등 커널의 자원을 요청할 때 발생

    User Space와 Kernel Space의 경계에서 일어남

    • ARM(라즈베리파이)의 System Call

      SVC (Supervisor Call) 명령어

      사용자 모드에서 SVC #0 실행 → 커널 모드로 전환 → 시스템 호출 번호 확인 → 작업 수행 → 결과 반환

4.2 파일 처리와 표준 입출력

✅ 파일 I/O

  • 고수준 입출력

    FILE 구조체를 사용하고, User Space에서 동작

    내부적으로 버퍼링과 오류 처리 등을 하므로 편리하지만 속도가 느

    C의 경우 fopen / fread / fwrite / fclose

    FILE *fp : 사용자 공간에 있는 구조체(버퍼, 파일 위치, 상태 정보 포함)

    fread: 내부 버퍼에서 먼저 읽고, 부족하면 read() 시스템 호출

  • 저수준 입출력

    정수 파일 디스크립터(fd)를 사용하고, Kernel Space에서 동작

    직접 커널 호출(ARM의 SVC)을 하므로 빠르고, 디바이스 드라이버 개발에 필수

    C의 경우 open / read / write / close

    int fd : 커널이 할당한 파일 디스크립터(0:stdin, 1:stdout, 2:stderr)

    open : ARM SVC 호출 → 커널 파일시스템 접근 → fd 반환

✅ Everything is a file

  • Everything is a file

    Unix에서는 모든 자원을 파일처럼 open/read/write/close 의 매커니즘으로 동일하게 처리함

    하드웨어의 제어가 C 파일 I/O처럼 편리하게 이루어짐

    1. 하드디스크

      예: /dev/hda1, /dev/hda2

    2. 디렉토리

    3. 키보드, 모니터

      예: /dev/stdin, /dev/stdout

    4. 네트워크 카드, 사운드 카드

    5. 시스템 정보(kernel)

      각 프로세스 또한 디렉토리로 접근 가능

      예: /proc , cpuinfo

    6. 드라이버 정보(device driver)

      예: /sys

✅ 파일의 종류

  • 파일의 종류

    종류표시설명
    일반 파일-데이터 저장 (텍스트, 바이너리, 실행파일)
    디렉토리d파일/디렉토리 목록
    심볼릭 링크l다른 파일/디렉토리 가리키는 텍스트 경로
    하드 링크-동일 inode 공유 (같은 데이터 여러 이름)
    디바이스b/c하드웨어
    소켓s네트워크/IPC 통신
    파이프p프로세스 간 데이터 전달

✅ 파일 열기

  • 파일 열고 사용하는 순서

    openread/writeclose

  • File open system call

    파일의 존재 여부 확인

    파일이 존재하지 않을 경우 생성 여부 결정

    파일에 대한 권한 확인

  • open() 시스템 콜을 이용한 파일 열기

    파일을 오픈하거나 생성할 때 사용하는 시스템 콜

    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    int open(const char *pathname, int oflag, mode_t mode)
  • 파일 오픈 플래그

    플래그의미라즈베리파이 예시
    O_RDONLY읽기 전용/proc/cpuinfo
    O_WRONLY쓰기 전용/sys/class/gpio/gpio17/value
    O_RDWR읽기+쓰기/home/pi/data.txt
    O_CREAT없으면 생성 (mode 필요)로그 파일
    O_TRUNC열 때 내용 삭제 (크기 0)새 로그 시작
    O_APPEND끝에 추가로그 파일
  • 파일 모드

    O_CREAT 사용할 때 세 번째 인자로 파일 모드(8진수)를 통해 권한 지정 가능

    open("new.txt", O_RDWR | O_CREAT, 0666);  // rw-rw-rw- (umask 적용 후 0644)
    open("gpio.txt", O_WRONLY | O_CREAT, 0600); // rw------- (root만)
  • 파일 디스크립터 (fd)

    커널이 프로세스별로 할당하는 정수 값

    open 을 처음 호출하면 3번부터 255번 사이의 가장 낮은 빈 번호를 할당

✅ 파일 읽기

  • File read system call read()

    파일 디스크립터(fd)에서 지정된 바이트 수만큼 데이터를 사용자 버퍼로 복사하는 함수

    파일로부터 데이터를 읽기 위한 시스템 콜 함수

    #include <unistd.h>
    ssize_t read(int fd, void *buf, size_t count);

    fdopen() 반환값으로 받은 파일 디스크립터 번호 (stdin=0, 파일=3 등)

    buf : 읽은 데이터를 저장할 사용자 공간 버퍼

    count : 요청 바이트 수

    반환값의미
    >0실제 읽은 바이트 수 (count 이하)
    0EOF (파일 끝, 파이프 종료)
    -1오류, errno 확인
  • 예제

    memset : 버퍼를 초기화할 때 사용하면 쓰레기값을 방지

    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <string.h>
    #include <errno.h>
    
    int main() 
    {
        int fd = open("/home/pi/data.txt", O_RDONLY);
        if (fd == -1) 
        {
            perror("open");
            return 1;
        }
        
        char buf[256];
        memset(buf, 0, sizeof(buf));          // 버퍼 초기화
        
        ssize_t n = read(fd, buf, sizeof(buf)-1);  // 최대 255바이트 읽기
        if (n == -1) 
        {
            perror("read");                   // EAGAIN, EINTR 등
        } 
        else if (n == 0) 
        {
            printf("파일 끝 (EOF)\n");
        } 
        else 
        {
            buf[n] = '\0';                    // 문자열 종료
            printf("읽음 %zd 바이트: %s\n", n, buf);
        }
        
        close(fd);
        return 0;
    }
  • Non-blocking 읽기 옵션 (O_NONBLOCK)

    기본 (blocking) : 데이터가 들어오지 않으면 무한 대기

    read(fd, buf, N);  // N바이트 올 때까지 블록

    Non-blocking 설정 : 데이터가 없으면 바로 반환

    #include <fcntl.h>
    
    int flags = fcntl(fd, F_GETFL);           // 현재 플래그 가져오기
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);   // O_NONBLOCK 추가
    
    ssize_t n = read(fd, buf, 100);
    if (n == -1 && errno == EAGAIN) {
        printf("데이터 없음, 바로 반환\n");   // non-blocking 특성
    }

✅ 파일 쓰기

  • File write system call write()

    버퍼에 있는 데이터를 특정 파일에 기록해 달라고 커널에 요청하는 역할

    write()를 사용하려면 파일이 쓰기 가능한 모드(O_WRONLY, O_RDWR 등)로 열려야 함

    #include <unistd.h>
    ssize_t write(int fd, const void *buf, size_t count);

    fd : open() 으로 얻은 파일 디스크립터 번호로, 작업을 할 파일을 가리킴

    count : 버퍼에서 요청한 작업 바이트 크기

    buf : 파일에 쓸 데이터가 들어 있는 메모리 주소

    반환값의미
    >0실제로 쓴 바이트 수, 항상 count와 같다는 보장은 없음
    0count가 0이면 실질적인 쓰기 작업 없이 0을 반환
    -1오류, errno 확인
  • read()write()의 buf 차이

    read(int fd, void *buf, size_t count)

    커널이 파일에서 사용자 버퍼로 데이터를 복사하므로, buf는 수정 가능한 포인터

    write(int fd, const void *buf, size_t count)

    커널이 사용자 버퍼 → 파일/디바이스 방향으로 읽기만 하므로, buf는 읽기 전용(const)으로 선언

✅ 파일 닫기

  • File close system call close()

    해당 파일 디스크립터를 더 이상 사용하지 않으므로, 커널의 자원을 회수하라는 의미의 시스템 콜

    #include <unistd.h>
    
    int close(int fd);
    반환값의미
    0정상 종료, 해당 fd에 연결된 커널 자원 해제됨
    -1실패, errno 확인

✅ 파일 탐색

  • 파일 탐색

    lseek() 시스템 콜로, 현재 fd에서 읽기/쓰기가 진행될 파일 위치(pos)를 옮기는 것

    파일 위치 값만 갱신하고 실제로 데이터 입출력을 하는 등의 다른 동작은 일어나지 않음

    성공하면 이동 후의 파일 위치(pos)를 반환하고, 실패 시에는 -1 반

    #include <sys/types.h>
    #include <unistd.h>
    
    off_t lseek(int fd, off_t offset, int whence);

    fd : open() 으로 얻은 파일 디스크립터

    offset : 이동하고 싶은 거리를 바이트 단위로 나타내며 양수/음수 모두 가능

    whence : 이동의 기준점을 지정하는 상수

  • whence(origin)에 따른 동작

    상수기준점 의미동작 예시 설명
    SEEK_SET파일의 시작(0 바이트 위치) 기준시작에서 offset만큼 떨어진 위치로 이동
    SEEK_CUR현재 파일 위치(pos) 기준지금 위치에서 offset만큼 앞/뒤로 이동
    SEEK_END파일의 끝(EOF) 기준파일 끝에서 offset만큼 앞/뒤로 이동

✅ Multiplexed I/O

  • Multiplexed I/O(입출력 다중화)

    하나의 프로세스 또는 스레드가 여러 파일 디스크립터를 동시에 감시하고, 입출력할 준비가 된 fd를 골라서 처리하는 방법

  • 입출력 다중화의 대표적인 방식

    • 동기성을 유지하면서 프로세스/스레드 사용

      각 fd에 전담 프로세스나 스레드를 하나씩 붙이는 방식

      메인 프로세스가 open()으로 fd를 만들고 나면 새 연결/파일마다 fork()로 프로세스를 생성하거나 pthread_create() 로 스레드를 생성

      각 프로세스/스레드는 맡은 fd에 대해 동기적인 blocking I/O(read(), write())만 사용

    • select()를 사용

      단일 프로세스에서 여러 개의 fd를 관리하면서 커널에 준비된 fd를 알려달라고 요청하는 방식

      여러 개의 파일을 파일 디스크립터 배열(fd_set)에 넣고 관리

      적은 스레드로 많은 fd를 효율적으로 처리할 수 있음

  • select()를 사용한 입출력 다중화

    #include <sys/select.h>
    
    int select(int nfds,
               fd_set *readfds,
               fd_set *writefds,
               fd_set *exceptfds,
               struct timeval *timeout);

    nfds : 감시할 fd 중 가장 큰 값 + 1

    readfds : 읽기 가능 여부를 감시할 fd 집합(예: 수신 대기 소켓, stdin 등)

    writefds : 쓰기 가능 여부를 감시할 fd 집합(예: 송신 버퍼 여유 확인)

    exceptfds : 예외(에러, out-of-band) 감시용 집합(주로 소켓)

    timeout : 대기 시간으로, NULL이면 준비될 때까지 무한대기, 0이면 즉시 복귀, 값 지정 시 해당 시간만 대기

  • FD 관리를 위한 fd_set 과 FD 매크로

    fd_set : 여러 개의 fd를 배열처럼 관리하기 위해 커널이나 라이브러리가 제공하는 비트 집합

    내부적으로 최대 1024개 정도의 fd를 비트 단위로 표현하며, 배열같은 느낌의 비트마스크 구조

    fd_set set;
    
    FD_ZERO(&set);      // fd 집합을 0으로 초기화
    FD_SET(fd, &set);   // fd 추가, fd의 비트를 1로 설정
    FD_CLR(fd, &set);   // fd 제거, fd의 비트를 0으로 설정
    FD_ISSET(fd, &set); // fd가 집합에 포함되어 있는지 확인
  • select를 사용한 입출력 다중화 흐름

    1. 감시할 fd 중 최대값+1을 nfds에 준비
    2. FD_ZERO()로 집합 초기화
    3. FD_SET()으로 감시할 fd들을 집합에 추가
    4. select() 호출로 이벤트(읽기/쓰기 가능) 발생까지 대기
    5. 반환 후 FD_ISSET()으로 어떤 fd가 준비됐는지 확인하고 해당 fd에 대해 read()/write() 수행

4.3 파일 정보와 권한

✅ 파일의 권한과 모드

  • 파일 권한과 소유권

    리눅스는 다중 사용자 OS로, 모든 파일에 소유자(owner), 그룹(group), 기타(other)에 대한 권한이 존재함

    권한은 읽기(r=4), 쓰기(w=2), 실행(x=1) 세 가지를 조합하여 보통 8진수 3자리로 표현

    기본 생성 권한에서 umask를 뺀 값을 사용 (예: 0644 → rw-r--r-- )

    일반 파일뿐만 아니라 디렉터리, 디바이스, 프로세스 정보, 드라이버 인터페이스도 모두 파일로 표현되고 권한을 가짐

    시스템 자원에 대한 접근 제어도 파일 권한으로 통합 관리

  • 파일의 소유권 변경하기

    chmod / chmod() : 권한 변경

    chown / chown() : 소유자 변경

    fchown() : 열린 fd로 소유자 변경

    lchown() : 심볼릭 링크 자체의 소유자 변경

  • umask와 기본 생성 권한

    새 파일의 기본 권한은 파일 0666, 디렉터리 0777에서 umask 값을 뺀 값으로 설정됨

    umask 명령어 / umask() 함수를 사용하여 파일을 생성할 때 적용할 마스크 값을 설정 가능

  • 파일의 종류와 권한, 모드 확인

    stat() 시스템 콜을 사용하면 파일의 종류, 권한, 크기, 소유자, 시간 정보를 확인할 수 있음

    파일에 대한 정보를 stat 구조체 형태로 돌려줌

    #include <sys/stat.h>
    
    int stat(const char *pathname, struct stat *buf);   // 경로로 조회
    int fstat(int fd, struct stat *buf);                // fd로 조회
    int lstat(const char *pathname, struct stat *buf);  // 링크 자체 조회
  • struct stat 주요 정보

    mode_t st_mode : 파일 종류 + 권한 비트 (S_IFREG, S_IRUSR 등)

    uid_t st_uidgid_t st_gid : 소유자/그룹 ID

    off_t st_size : 파일 크기

    time_t st_mtime : 마지막 수정 시간 등

  • inode와 파일 복사 비교

    일반 복사(cp) : 새 inode를 생성하며, 복사 시점의 데이터만 같은 완전히 독립된 파일

    링크 : 기존 inoe를 재사용하여 디스크 공간을 절약하고 데이터의 일관성을 유지함

    # 테스트 (라즈베리파이 SD카드)
    echo "test" > origin.txt
    cp origin.txt copy.txt      # 새 inode 생성
    ln origin.txt hard.txt      # hard link (같은 inode)
    ln -s origin.txt sym.txt    # symbolic link (별도 inode)
    
    ls -li *.txt
    # 123456 -rw-r--r-- origin.txt
    # 789012 -rw-r--r-- copy.txt    ← 다른 inode
    # 123456 -rw-r--r-- hard.txt    ← 같은 inode
    # 345678 lrwxrwxrwx sym.txt     ← 별도 inode (l=링크)
  • Hard Link

    같은 inode를 공유하며, 디렉토리 엔트리(덴트리)에 inode 번호만 추가됨

    원본을 삭제해도 inode의 링크 카운트가 1 이상이면 데이터가 유지됨

    같은 파일 시스템 내에서만 사용 가능하며 디렉토리에는 사용 불가

    하나의 파일을 여러 디렉토리에서 공유하고 싶을 때 유용하고, 백업/중복 방지 역할 가능

  • Symbolic Link (Soft Link)

    별도의 inode를 가지며, 원본 파일의 경로 문자열만 저장

    장치 번호와 inode 번호를 함께 가지고 있어 디렉토리 링크, 다른 장치나 파일 시스템 간에도 링크 가능

    원본 삭제 시에는 파일이 깨짐

✅ 표준 입출력과 리다이렉션

  • 표준 입출력 파일 디스크립터

    fd 번호이름기본 연결
    0stdin키보드 (/dev/stdin)
    1stdout모니터 (/dev/stdout)
    2stderr모니터 (/dev/stderr)
  • 시스템 프로그래밍 관점에서 Redirection 구현

    셸의 >2> 리다이렉션은 dup2() 로 구현 가능하고 백그라운드 명령 실행 시 자주 사용됨

    newfdoldfd 의 복사본처럼 동작하여 같은 파일을 가리킴

    이때 파일 디스크립터 flag까지 복사하지는 않고, newfd가 이미 열려 있으면 자동으로 close() 후 재할당

    #include <unistd.h>
    
    int dup2(int oldfd, int newfd);

    oldfd : 복사할 원본 fd (예 : 새로 연 파일의 fd=3)

    newfd : 덮어쓸 대상 fd (예 : stdout=1)

  • 파일 디스크립터 테이블(FDT)

    각 프로세스는 0~255 크기의 FDT 배열을 가지며, stdin, stdout, stderr을 기본적으로 열어 둠

    파일 열기에 성공하면 FDT에서 3번부터 순서대로 할당

    fork() 후에는 자식도 동일한 FDT를 복사하여 갖게되며, dup2 로 독립 조작

    [0: /dev/stdin, 1: /dev/stdout, 2: /dev/stderr, 3: log.txt, 4: gpio, ...]

✅ 입출력 버퍼 비우기

  • 버퍼

    입출력 성능을 높이기 위해 데이터를 모아 두었다가 한 번에 장치로 보내거나 받아들이는 메모리 공간

    버퍼의 크기는 대체로 1024바이트

    write() 를 1바이트씩 2048번 쓰는 것보다 1024바이트씩 2번 호출하는 편이 시스템 콜 횟수가 줄어 효율적

  • 입출력 버퍼

    고수준 입출력 함수를 사용하면 먼저 버퍼에 내용이 쌓이고 바로 입출력이 일어나지 않음

    버퍼가 일정 크기 이상 차거나 줄바꿈 문자 입력/종료 시 실제로 입출력이 일어나는 형태

  • I/O 스케줄러

    커널 내부에서 버퍼에 쌓인 데이터를 언제 내보낼지 결정하는 레이어

    stdin/stdout, 디스크 · 드라이버와 I/O를 수행할 때 실질적인 타이밍을 조절함

  • fflush() 로 버퍼 비우기

    #include <stdio.h>
    
    int fflush(FILE *stream);

    출력 버퍼에 쌓인 데이터를 즉시 목적지로 전송하고 버퍼를 비우는 역할

    실시간 로그 확인, 사용자에게 즉시 입력 받기 등 버퍼가 채워질 때까지 기다릴 수 없는 경우 유용함

    • 예시

      fflush(stdout); : 표준 출력 버퍼를 강제로 비움 → 지금까지 printf()한 내용이 즉시 화면/리다이렉션된 파일로 출력

      fflush(fp); : 파일 스트림 fp에 대한 출력 버퍼를 디스크로 출력

      fflush(NULL); : 모든 출력 스트림을 한 번에 플러시

4.4 디렉토리와 시간 처리

✅ 디렉토리

  • 현재 디렉토리

    chdir() : 작업 디렉토리 변경 함수로, 셸에서 사용하던 cd 와 유사함

    getcwd() : 현재 경로를 얻는 함수

  • 디렉토리 삭제, 이동, 접근

    mkdir() / rmdir() : 디렉토리 생성/삭제 함수

    rename() : 디렉토리 이동

    opendir() / readdir() / closedir() : 디렉토리 내용 접근

  • dirent 구조체

    readdir() 이 반환하는 구조체

    디렉토리 안의 각 엔트리(파일/디렉토리)를 나타냄

    #include <dirent.h>
    
    struct dirent {
        ino_t          d_ino;    // inode 번호
        off_t          d_off;    // 디렉토리 내 offset
        unsigned short d_reclen; // 레코드 길이
        unsigned char  d_type;   // 타입(DT_REG, DT_DIR 등 시스템마다 다를 수 있음)
        char           d_name[]; // 항목 이름 (널 종료 문자열)
    };

✅ 유닉스의 시간

  • 유닉스의 시간

    Epoch(1970-01-01 00:00:00 UTC)부터 지금까지 흐른 초 단위 정수로 시간을 표현

    time() 함수를 사용하면 이 값을 time_t 형 정수로 얻을 수 있음

    #include <time.h>
    
    time_t time(time_t *timep);
  • tm 구조체

    연도, 월, 일 등을 분리해서 처리할 때 사용하면 편리한 구조체

    struct tm {
        int tm_sec;   // 초 [0, 60]
        int tm_min;   // 분 [0, 59]
        int tm_hour;  // 시 [0, 23]
        int tm_mday;  // 일 [1, 31]
        int tm_mon;   // 월 [0, 11] (0=1월)
        int tm_year;  // 1900년부터의 년수
        int tm_wday;  // 요일 [0, 6] (0=일요일)
        int tm_yday;  // 연중 일수 [0, 365]
        int tm_isdst; // 서머타임 여부
    };
  • gmtime()

    struct tm *gmtime(const time_t *timep);

    time_t 를 UTC 기준의 tm 구조체로 변환

  • localtime()

    struct tm *localtime(const time_t *timep);

    time_t 를 로컬 타임존 기준의 tm 구조체로 변환

    시스템의 시간대 설정을 반영

  • cal 명령어

    터미널에서 cal 을 입력하면 현재 혹은 지정한 연도/월의 달력을 출력


2️⃣ 느낀 점

파일은 다루어도 다루어도 끝이 없다는 생각이 들었다.
이제 개념 정리는 끝내버려!

0개의 댓글