Embedded Blog — Embedded 공부 공간입니다.
'Linux' 태그의 글 목록
// list

Linux

리눅스 파일 접근 권한과 실행 루트 관리(chmod, chown, chroot)

Summary: chmod는 파일 모드 비트를 바꿔 사용자별 접근 범위를 정하고, chown은 그 권한을 적용받는 소유자와 그룹을 바꾼다. chroot는 이 둘과 달리 프로세스가 절대 경로를 해석하는 루트를 변경하며, 별도의 실행 환경을 구성할 수 있지만 완전한 보안 격리를 제공하지는 않는다.

chmod

chmod(Change Mode)는 파일과 디렉터리의 접근 권한을 변경한다. 파일에는 소유자, 그룹, 그 외 사용자라는 세 범주가 있고, 각 범주에는 읽기·쓰기·실행 권한을 서로 다르게 줄 수 있다. chmod를 사용하는 이유는 이 접근 범위를 실제 용도에 맞게 나누기 위해서다. 일반 파일에서는 내용을 읽고 수정하고 실행할 수 있는지를 정하고, 디렉터리에서는 목록 조회, 내부 파일 생성·삭제, 경로 탐색 가능 여부를 조정한다.

 

기본 구문은 다음과 같다.

chmod [OPTION]... {MODE | --reference=REF_FILE} FILE...

MODE를 직접 지정하거나 --reference로 다른 파일의 모드를 가져올 수 있다. 권한은 기호 모드와 8진수 모드 중 하나로 표현한다. 파일 모드 비트를 바꿀 수 있는 프로세스는 유효 사용자 ID가 파일 소유자의 사용자 ID와 일치하거나, 해당 작업에 필요한 권한을 가지고 있어야 한다. 명령이 성공하면 종료 상태는 0, 실패하면 0이 아닌 값이다.

파일 모드 비트

파일 모드 비트는 일반 접근을 제어하는 권한 비트와 일부 파일에만 영향을 주는 특수 모드 비트로 구성된다. 일반 권한을 해석할 때는 먼저 권한의 대상이 누구인지 확인해야 한다.

기호 범주 의미
u user 파일 소유자
g group 파일 그룹에 속한 사용자
o others 그 외 사용자
a all 모든 사용자, ugo와 동일

rwx의 의미는 일반 파일과 디렉터리에서 다르다.

권한 일반 파일 디렉터리
r 4 내용 읽기 항목 목록 조회
w 2 내용 변경 내부 파일 생성·삭제
x 1 프로그램으로 실행 내부 파일 접근·경로 탐색

특히 디렉터리의 w와 x를 일반 파일의 쓰기·실행과 같은 의미로 읽으면 권한을 잘못 판단하기 쉽다. 디렉터리에서 w는 내부 항목을 만들거나 지우는 동작에 관여하고, x는 그 디렉터리를 경로의 일부로 사용해 내부 파일에 접근하는 권한이다.

기호 모드

기호 모드는 현재 권한을 기준으로 일부 비트만 추가하거나 제거할 때 적합하다. 표현은 사용자, 연산자, 권한 순서이며 내부에 공백을 넣지 않는다.

[ugoa][+-=][rwxXst]

연산자는 기존 권한을 어떻게 다룰지 결정한다.

연산자 의미
+ 기존 권한에 지정 권한 추가
- 기존 권한에서 지정 권한 제거
= 기존 권한을 지정 권한으로 교체

r, w, x는 읽기, 쓰기, 실행·탐색을 뜻한다. 대문자 X는 디렉터리이거나 이미 실행 권한이 하나라도 있는 파일에만 실행·탐색 권한을 적용한다. s는 u와 결합하면 set-user-ID, g와 결합하면 set-group-ID를 가리키고, t는 sticky bit를 가리킨다. 서로 다른 변경을 한 번에 적용할 때는 쉼표로 구분한다.

chmod a=rw foo
chmod go-w foo
chmod u=rwx,g=rx,o= foo
chmod a+X dir

첫 번째 명령은 모든 사용자에게 읽기·쓰기만 허용한다. 두 번째는 소유자를 제외한 그룹과 그 외 사용자에게서 쓰기 권한만 제거하므로, 나머지 권한에는 영향을 주지 않는다. 세 번째처럼 =를 사용하면 각 사용자 범주의 권한을 명시한 값으로 교체한다. 마지막 명령의 X는 디렉터리와 기존 실행 파일만 선별한다는 점에서 소문자 x와 구분된다.

8진수 모드

8진수 모드는 소유자, 그룹, 그 외 사용자의 rwx 조합을 각각 하나의 숫자로 나타낸다. 각 자리는 읽기 4, 쓰기 2, 실행·탐색 1을 더해 계산한다.

권한 계산
0 --- 권한 없음
1 --x 1
2 -w- 2
3 -wx 2 + 1
4 r-- 4
5 r-x 4 + 1
6 rw- 4 + 2
7 rwx 4 + 2 + 1

세 자리 앞에는 특수 권한을 나타내는 자리가 올 수 있다.

[특수 권한][소유자][그룹][그 외 사용자]

숫자 모드는 항상 8진수로 해석되므로 선행 0은 필요하지 않다. 기호 모드보다 짧지만, 일반적으로 기존 권한의 일부를 조정하기보다 전체 권한을 절대값으로 설정한다는 차이가 있다.

모드 기호 표현 의미
644 u=rw,g=r,o=r 소유자 읽기·쓰기, 나머지 읽기
664 ug=rw,o=r 소유자·그룹 읽기·쓰기, 나머지 읽기
751 u=rwx,g=rx,o=x 소유자 전체, 그룹 읽기·실행, 나머지 실행
4751 u=srwx,g=rx,o=x 751 + set-user-ID
chmod 644 foo
chmod ug+x file

chmod 644 foo는 세 사용자 범주의 권한을 한 번에 확정한다. 반면 chmod ug+x file은 소유자와 그룹에 실행 권한만 추가한다. 최종 상태를 명확히 지정할 때는 숫자 모드가 짧고, 현재 설정을 유지하면서 특정 권한만 바꿀 때는 기호 모드가 변경 범위를 더 직접적으로 드러낸다.

특수 권한

특수 권한은 일반적인 읽기·쓰기·실행과 별도로 실행 프로세스의 유효 ID나 디렉터리의 파일 관리 방식에 영향을 준다.

8진수 값 기호 명칭  효과
4000 u+s set-user-ID 실행 시 프로세스의 유효 사용자 ID를 파일 소유자로 설정
2000 g+s set-group-ID 실행 시 프로세스의 유효 그룹 ID를 파일 그룹으로 설정, 디렉터리에서는 새 항목이 디렉터리 그룹을 상속
1000 +t sticky bit 디렉터리에서 권한 없는 사용자의 파일 삭제·이름 변경을 파일 또는 디렉터리 소유자로 제한
chmod u+s program
chmod a-s program
chmod +t shared_dir
chmod 6755 shared_dir

디렉터리에 set-group-ID가 설정되어 있으면 그 안에 생긴 새 파일은 디렉터리의 그룹을 상속하고, 새 하위 디렉터리는 set-group-ID까지 상속한다. 여러 사용자가 같은 그룹을 기준으로 파일을 다루는 디렉터리에서는 소유 그룹이 이어지는 방식 자체가 권한 관리의 일부가 된다.

 

GNU chmod에서 디렉터리의 set-user-ID와 set-group-ID를 제거할 때는 제거 의도를 명시해야 한다. chmod a-s DIR, chmod =755 DIR, chmod 00755 DIR이 이에 해당한다. 일반 권한 세 자리만 보고 특수 비트도 항상 함께 지워질 것이라고 가정해서는 안 된다.

주요 옵션

chmod의 옵션은 재귀 처리, 출력 범위, 참조 파일 사용, 심볼릭 링크 처리 방식에 집중되어 있다.

옵션 의미
-R, --recursive 디렉터리와 내부 항목의 권한을 재귀적으로 변경
-c, --changes 실제 변경된 파일만 출력
-f, --silent, --quiet 변경 실패 오류 메시지 생략
-v, --verbose 처리한 모든 파일의 동작·비동작 출력
--reference=REF_FILE 참조 파일과 동일한 모드로 변경
--preserve-root /에 대한 재귀 변경 거부
--dereference 심볼릭 링크 자체가 아닌 대상에 적용
-h, --no-dereference 지원 시스템에서 심볼릭 링크 자체에 적용
-H -R 사용 시 명령행 인수인 디렉터리 심볼릭 링크만 탐색
-L -R 사용 시 발견한 모든 디렉터리 심볼릭 링크 탐색
-P 심볼릭 링크 탐색 안 함

심볼릭 링크를 명령행 인수로 직접 지정하면 기본적으로 링크가 가리키는 파일의 권한을 바꾼다. 반면 재귀 탐색 과정에서 발견한 심볼릭 링크는 기본적으로 무시한다. 같은 링크라도 명령행에서 직접 받은 경우와 디렉터리 아래에서 발견한 경우의 처리가 다르다.

 

-R과 -L 또는 역참조 옵션을 결합하면 범위가 더 넓어진다. 재귀 순회 중 공격자가 심볼릭 링크의 대상을 바꿀 수 있다면 원래 의도하지 않은 파일까지 권한 변경 대상이 될 수 있다. 루트 디렉터리를 재귀적으로 건드리지 않도록 하는 --preserve-root도 변경 범위를 통제하는 옵션이다.

 

chown

chown(Change Owner)은 파일과 디렉터리의 사용자 소유권 또는 그룹 소유권을 변경한다. chmod가 소유자·그룹·그 외 사용자에게 어떤 권한을 줄지 정한다면, chown은 그중 소유자와 그룹 자리에 누가 들어갈지를 바꾼다. 파일을 관리할 사용자나 그룹이 달라졌거나 여러 파일의 소유권을 기준 파일에 맞춰야 할 때 이 명령을 사용한다.

chown [OPTION]... {NEW_OWNER | --reference=REF_FILE} FILE...

소유자와 그룹은 공백 없이 다음 형식으로 지정한다.

[OWNER][:[GROUP]]
형식 사용자 소유자 그룹 소유자
OWNER OWNER로 변경 유지
OWNER:GROUP OWNER로 변경 GROUP으로 변경
OWNER: OWNER로 변경 OWNER의 로그인 그룹으로 변경
:GROUP 유지 GROUP으로 변경, chgrp와 같은 기능
: 유지 유지

OWNER와 GROUP에는 이름 또는 숫자 ID를 넣을 수 있다. 숫자 ID가 같은 문자열의 이름과 혼동될 가능성을 피하려면 앞에 +를 붙일 수 있다. 사용자와 그룹의 구분자는 :를 사용한다. 오래된 스크립트에서 보이는 . 표기는 이식성이 없고 GNU에서도 경고가 발생하며 지원이 제거될 수 있다.

 

Linux에서는 사용자 소유자를 바꾸는 작업을 CAP_CHOWN을 가진 특권 프로세스만 수행할 수 있다. 파일 소유자는 그룹을 자신이 속한 그룹 중 하나로 바꿀 수 있고, CAP_CHOWN을 가진 프로세스는 임의의 그룹으로 변경할 수 있다. 따라서 다음 세 명령은 문법만 다른 것이 아니라 변경 범위와 필요한 권한도 다르다.

chown root /u
chown root:staff /u
chown -hR root /u

첫 번째 명령은 사용자 소유자만 root로 바꾸고 그룹은 유지한다. 두 번째는 사용자 소유자와 그룹을 함께 바꾼다. 세 번째는 /u와 그 아래 항목의 소유자를 재귀적으로 바꾸되, 심볼릭 링크에서는 링크 자체를 대상으로 삼는다. 성공 시 종료 상태는 0, 실패 시 0이 아닌 값이다.

주요 옵션

옵션 의미
-R, --recursive 디렉터리와 내부 항목의 소유권을 재귀적으로 변경
-c, --changes 실제 변경된 파일만 출력
-f, --silent, --quiet 변경 실패 오류 메시지 생략
-v, --verbose 처리한 모든 파일 출력
--from=OLD_OWNER 현재 소유권이 지정값과 일치할 때만 변경
--reference=REF_FILE 참조 파일과 동일한 사용자·그룹으로 변경
--dereference 심볼릭 링크 자체가 아닌 대상에 적용, 비재귀 작업의 기본값
-h, --no-dereference 심볼릭 링크 대상 대신 링크 자체에 적용
--preserve-root /에 대한 재귀 변경 거부
-H -R 사용 시 명령행 인수인 디렉터리 심볼릭 링크만 탐색
-L -R 사용 시 발견한 모든 디렉터리 심볼릭 링크 탐색
-P 심볼릭 링크 탐색 안 함, 재귀 작업의 기본값

--from=OLD_OWNER는 파일의 현재 소유권이 예상한 값과 일치할 때만 변경한다. 소유권을 확인한 뒤 별도의 명령으로 바꾸는 방식보다 확인과 변경 사이의 간격이 짧고, 예상과 다른 소유권의 파일이 함께 바뀔 가능성도 줄어든다.

 

심볼릭 링크 처리는 비재귀 작업과 재귀 작업을 나눠 봐야 한다. 비재귀 작업에서는 링크 대상을 따라가는 --dereference가 기본이고, 재귀 작업에서는 링크를 탐색하지 않는 -P가 기본이다. -R과 -L 또는 역참조를 함께 사용하면 chmod와 마찬가지로 순회 중 바뀐 링크가 의도하지 않은 파일을 가리킬 수 있다.

 

소유권 변경은 권한 비트와 완전히 독립적인 작업도 아니다. 처리 결과 set-user-ID 또는 set-group-ID가 제거될 수 있으며, 실제 동작은 시스템 정책과 시스템 호출에 따라 달라진다. 소유자만 바꾼 뒤 기존 특수 권한이 그대로 남아 있다고 전제해서는 안 되는 이유다.

 

chroot

chroot(Change Root)는 명령을 실행하기 전에 프로세스의 루트 디렉터리를 지정한 디렉터리로 바꾼다. 이후 절대 경로는 원래 시스템의 /가 아니라 NEWROOT를 시작점으로 해석된다. 새 루트 안에 실행 파일, 라이브러리, 데이터 파일을 별도로 배치하고 그 구성을 기준으로 명령을 실행할 때 사용한다.

chroot [OPTION]... NEWROOT [COMMAND [ARGS]...]

동작 순서는 세 단계다.

  1. 존재하는 NEWROOT를 새 루트 디렉터리로 변경한다.
  2. 작업 디렉터리를 새 루트 내부의 /로 변경한다.
  3. COMMAND와 ARGS를 실행한다.

일반적인 절대 경로는 기존 /부터 탐색하지만, chroot 이후에는 같은 / 표기가 NEWROOT를 가리킨다. COMMAND를 생략하면 $SHELL을 사용하고, 이 환경 변수가 설정되어 있지 않으면 /bin/sh를 -i 옵션으로 실행한다. 별도 옵션이 없다면 명령의 실행 자격 증명은 호출 프로세스와 같다.

 

많은 시스템에서 이 명령은 슈퍼유저만 실행할 수 있다. Linux에서는 사용자 네임스페이스 안에서 CAP_SYS_CHROOT가 필요하다. chroot는 경로의 출발점을 바꾸는 작업이므로 새 디렉터리를 준비하는 것만으로 끝나지 않고, 그 안에서 실행할 명령이 실제로 사용할 파일까지 함께 구성해야 한다.

주요 옵션

옵션 의미
--userspec=USER[:GROUP] 다른 사용자와 기본 그룹으로 명령 실행
--groups=GROUPS 쉼표로 구분한 보조 그룹 지정
--groups='' --userspec에 따른 보조 그룹 조회 비활성화
--skip-chdir 새 루트 변경 후 작업 디렉터리를 /로 바꾸지 않음, NEWROOT가 기존 /일 때만 허용

--userspec은 새 루트에서 명령을 실행할 사용자와 기본 그룹을 지정하고, --groups는 보조 그룹 목록을 덮어쓴다. 사용자와 그룹 이름은 chroot 외부와 내부에서 모두 조회하며, 내부 조회가 성공하면 그 결과가 우선한다. 이름이 아니라 숫자 ID임을 분명히 할 때는 chown과 마찬가지로 앞에 +를 붙인다.

 

--skip-chdir는 이름 그대로 루트 변경 뒤의 작업 디렉터리 이동을 생략한다. 다만 NEWROOT가 기존 /일 때만 허용되므로, 일반적인 새 루트 구성에서 외부 작업 디렉터리를 그대로 유지하기 위한 옵션은 아니다.

실행 환경 구성

새 루트에서 명령을 찾더라도 실행에 필요한 파일이 그 안에 없다면 명령은 동작하지 않는다. 실행 파일의 링크 방식에 따라 준비 범위가 달라진다.

  • 정적 링크 실행 파일은 실행 파일 자체를 새 루트 내부에 배치한다.
  • 동적 링크 실행 파일은 실행 파일과 필요한 공유 라이브러리를 새 루트 내부의 올바른 경로에 함께 배치한다.
  • 실행 파일이 데이터, 상태 파일, 장치 파일을 요구하면 이 파일들도 필요한 위치에 준비한다.

동적 실행 파일의 공유 라이브러리는 ldd COMMAND로 확인한다. 예를 들어 정적으로 링크한 ls를 /tmp/empty/ls에 배치했다면 다음처럼 실행할 수 있다.

chroot /tmp/empty /ls -Rl /

여기서 명령에 적힌 /ls와 마지막 인수 /는 모두 원래 파일 시스템이 아니라 /tmp/empty를 루트로 해석한다. 새 루트 디렉터리 안의 구조가 실행 환경의 가시 범위를 결정하는 방식이다.

보안 특성

chroot는 경로 해석의 한 요소를 변경할 뿐, 완전한 샌드박스나 보안 격리 수단은 아니다. 명령 이름에 포함된 root가 관리자 계정을 뜻하는 것이 아니라 경로 탐색의 루트 디렉터리를 뜻한다는 점도 함께 구분할 필요가 있다.

 

chroot(2) 시스템 호출은 현재 작업 디렉터리를 바꾸지 않으며 열린 파일 디스크립터도 닫지 않는다. 현재 작업 디렉터리가 새 루트 밖에 있거나, 이미 열린 파일 디스크립터가 외부 파일을 가리키고 있다면 새 루트 바깥에 접근할 가능성이 남는다. GNU chroot 명령이 루트 변경 후 작업 디렉터리를 새 루트의 /로 옮기는 이유는 이 시스템 호출의 동작과 구분해서 봐야 한다. --skip-chdir를 지정하면 이 이동은 생략된다.

 

따라서 chroot가 보여 주는 파일 트리의 시작점이 바뀌었다는 사실과 프로세스가 완전히 격리됐다는 판단은 같지 않다. 이 명령으로 구성한 실행 환경을 보안 경계 자체로 해석할 수 없는 근거가 열린 파일 디스크립터와 현재 작업 디렉터리의 처리에 있다.

종료 상태

chroot는 자체 실패와 실행할 명령의 탐색·호출 실패를 서로 다른 종료 상태로 구분한다.

상태 의미
125 chroot 자체 실패
126 명령을 찾았으나 실행할 수 없음
127 명령을 찾을 수 없음
그 외 실행한 명령의 종료 상태

새 루트 안에 명령이 없으면 127, 파일은 있지만 필요한 조건을 갖추지 못해 호출할 수 없으면 126으로 구분된다. 실행까지 성공한 경우에는 chroot가 별도의 성공 값을 덮어쓰지 않고 실행한 명령의 종료 상태를 그대로 반환한다.

DHT22 단선 프로토콜과 PMS7003 UART 프레임 구조

Summary: 강의실 환경 모니터링 프로젝트에서 사용하는 두 센서의 통신 방식을 분석한다. DHT22는 마이크로초 펄스 폭으로 비트를 구분하는 단선 시리얼 40비트 프로토콜을, PMS7003은 UART 9600 8N1 위에서 고정 32바이트 프레임을 사용하며, 각각 리눅스에서 어떻게 읽고 검증하는지까지 정리했다.

Raspberry Pi 4 GPIO 핀맵

DHT22 (온습도 센서)

DHT22(AM2302)는 정전용량식 습도 소자와 서미스터(NTC)를 하나의 모듈에 담아 온도와 상대습도를 함께 측정하는 디지털 센서다. 측정 범위는 습도 0~100%RH(±2~5%RH), 온도 -40~80°C(±0.5°C)이며, 데이터는 단선(single-wire) 시리얼로 40비트를 출력한다.

 

배선은 VCC, GND, DATA 3선으로 단순하고 DATA에 풀업 저항이 필요하다. 이 프로젝트에서는 GPIO4에 연결했다.

 

사양에서 실제 코드 설계에 영향을 주는 항목은 샘플링 주기다. 센서 자체는 1Hz로 샘플링하지만 연속 요청 간격은 최소 2초가 필요하고, 그보다 짧게 재요청하면 이전 값이나 오류가 돌아온다. 즉 "빨리 여러 번 읽어서 평균 내는" 접근이 이 센서에서는 성립하지 않는다.

 

DHT22 단선 시리얼 프로토콜

단선 프로토콜은 데이터선 한 가닥으로 요청과 응답을 주고받는 반이중 방식이다. 이름 때문에 Dallas 1-Wire로 오해하기 쉬운데, DHT 계열은 자체 타이밍 규격을 쓰는 별개의 프로토콜이다.

 

통신 순서는 다음과 같다. MCU가 시작 신호로 데이터선을 최소 1ms LOW로 끌어내리고 20~40µs를 기다리면, 센서가 80µs LOW + 80µs HIGH로 응답한 뒤 40비트 데이터를 전송한다.

 

40비트의 구성은 습도 정수 8bit + 습도 소수 8bit + 온도 정수 8bit + 온도 소수 8bit + 체크섬 8bit이며, 체크섬은 앞 4바이트 합의 하위 8비트다.

 

비트 판별 방식이 이 프로토콜의 핵심이자 문제의 근원이다. 각 비트는 50µs LOW 뒤에 오는 HIGH의 유지 시간으로 구분되는데, 26~28µs면 0, 70µs면 1이다. 0과 1의 차이가 수십 마이크로초에 불과하므로, 일반 사용자 공간 프로세스가 GPIO를 폴링해서 이 폭을 재는 방식은 커널 스케줄링에 밀리는 순간 비트를 통째로 놓친다. 이 프로젝트에서 커널 dht11 IIO 드라이버를 쓰기로 한 것은 이 때문이다. 타이밍 측정을 커널에 위임하면 사용자 코드는 sysfs 파일(in_temp_input, in_humidityrelative_input)을 읽는 평범한 파일 I/O로 단순해진다.

 

PMS7003 (미세먼지 센서)

PMS7003(Plantower)은 레이저 산란 방식으로 입자 농도를 측정하는 디지털 미세먼지 센서다. 0.3~1.0 / 1.0~2.5 / 2.5~10µm 구간의 입자를 감지하며, 유효 범위 0~500µg/m³, 분해능 1µg/m³로 PM1.0/PM2.5/PM10 농도와 입자 개수를 UART로 출력한다.

 

전원 계통은 주의가 필요하다. 팬과 내부 로직은 5V로 구동하지만 통신 핀은 3.3V TTL이라 Raspberry Pi GPIO에 레벨 변환 없이 직결할 수 있다.

 

동작 모드는 두 가지다. 액티브 모드(기본)는 측정값을 자동으로 주기 전송하고, 패시브 모드는 요청이 있을 때만 응답한다. 이 프로젝트는 액티브 모드를 사용한다. 센서가 알아서 보내주므로 수신 측은 명령을 보낼 필요 없이 스트림을 읽기만 하면 되고, 결과적으로 RX 한 선만으로도 수신이 성립한다.

 

UART 통신

UART(Universal Asynchronous Receiver/Transmitter)는 클록선 없이 시작·정지 비트로 프레임을 구분하는 비동기 직렬 통신이다. 송신(TX)과 수신(RX) 두 선을 교차 연결하며, PMS7003의 설정은 9600bps, 데이터 8bit, 패리티 없음, 정지 1bit(8N1)이다.

 

연결은 센서 TX → Pi RX(GPIO15)다. 리눅스 구현은 /dev/ttyAMA0을 open()한 뒤 termios로 raw 모드와 9600bps를 설정하고 read()로 바이트를 받는 구조다. DHT22와 비교하면 대조가 뚜렷하다. UART는 하드웨어(UART 컨트롤러)가 비트 타이밍을 처리해 주므로, 사용자 공간에서 읽어도 DHT22 같은 타이밍 문제가 없다. 같은 "센서 읽기"라도 프로토콜 계층이 어디까지 하드웨어/커널에 내려가 있느냐에 따라 사용자 코드의 난이도가 갈린다.

 

PMS7003 32바이트 프레임 구조

액티브 모드의 출력 프레임은 고정 32바이트이고, 값은 빅 엔디언 16비트 워드 단위로 실린다.

바이트 내용
0–1 시작 문자 0x42 0x4D (ASCII "BM")
2–3 프레임 길이 (2×13+2 = 28)
4–9 PM1.0 / PM2.5 / PM10 농도 (CF=1, 표준 입자 기준)
10–15 PM1.0 / PM2.5 / PM10 농도 (대기 환경 기준)
16–27 0.1L당 입자 개수 (>0.3 / 0.5 / 1.0 / 2.5 / 5.0 / 10µm)
28 버전
29 오류 코드
30–31 체크섬

값 조립은 (상위바이트 << 8) | 하위바이트이고, 체크섬은 앞 30바이트(0~29)의 합이 30~31바이트 값과 일치해야 유효 프레임이다.

 

파싱할 때 헷갈리기 쉬운 지점이 두 곳 있다. 첫째, 같은 PM2.5가 CF=1 기준(바이트 6~7)과 대기 환경 기준(바이트 12~13)으로 두 번 실려 온다. 실내·일반 환경 측정에는 대기 환경 값을 쓰는 것이 맞고, 이 프로젝트는 바이트 12~13(PM2.5), 14~15(PM10)를 파싱한다. 둘째, 수신 시점이 프레임 경계와 일치한다는 보장이 없다. 액티브 모드 스트림을 중간부터 읽기 시작할 수 있으므로, 0x42 0x4D 헤더를 먼저 찾아 동기화한 뒤 나머지 30바이트를 받아야 한다.

 

동작 확인

설정이 끝난 상태에서 각 센서를 코드 없이 먼저 확인할 수 있다.

DHT22는 sysfs를 직접 읽는다. 값은 밀리 단위다(24300 = 24.3°C).

cat /sys/bus/iio/devices/iio:device0/in_temp_input
cat /sys/bus/iio/devices/iio:device0/in_humidityrelative_input

I/O error가 간헐적으로 나오는 것은 DHT22 응답 실패로, 이 센서의 정상 동작 범위다.

PMS7003은 원시 바이트에서 헤더가 주기적으로 보이는지 확인한다.

sudo stty -F /dev/ttyAMA0 9600 cs8 -cstopb -parenb raw
sudo hexdump -C -n 256 /dev/ttyAMA0

출력에서 42 4d가 32바이트 간격으로 반복되면 액티브 모드 송신이 정상이다.

 

데이터 경로 요약

두 센서의 값이 모두 유효할 때만 5분 간격으로 INSERT한다.

 

결론

두 센서는 "타이밍을 누가 책임지는가"에서 갈린다. DHT22의 단선 프로토콜은 µs 타이밍을 수신 측이 재야 하므로 커널 드라이버에 위임했고, PMS7003의 UART는 하드웨어가 타이밍을 처리하므로 사용자 공간 termios로 충분하다. 대신 PMS7003은 스트림 동기화와 체크섬 검증이라는 프레임 파싱 문제를 수신 코드가 책임져야 한다. 다음 편에서는 이 데이터를 받을 PostgreSQL 스키마를 설계한다.

Raspberry Pi에 Ubuntu Server 설치와 네트워크 설정

Summary: Raspberry Pi Imager로 Raspberry Pi 4에 Ubuntu Server를 설치하는 과정과, Raspberry Pi OS와 달리 mDNS가 없어 호스트명.local 접속이 되지 않는 문제를 다룬다. 해결 경로로 Netplan(YAML)을 이용한 네트워크 설정과 ip 명령을 통한 인터페이스·주소·라우팅 확인/수정을 정리하고, 필요 시 avahi를 설치해 mDNS를 복원하는 방법까지 이어진다.

Ubuntu Linux CLI 설치

설치는 Raspberry Pi Imager로 진행하며, 대상 보드는 Raspberry Pi 4다. Imager에서 디바이스와 OS, 저장 장치를 차례로 지정하는데, Ubuntu Server 이미지는 최상위 목록이 아니라 "Other general-purpose OS → Ubuntu → Ubuntu Server" 경로 안에 있다. 데스크탑 환경이 아닌 CLI 전용 서버 이미지를 선택한다는 점이 이후 모든 설정의 전제가 된다.

 

주목할 부분은 이미지를 굽기 전 커스터마이즈 단계다. hostname(home), 지역·시간·키보드 레이아웃, 사용자 이름·비밀번호, Wi-Fi(핫스팟), 그리고 SSH 사용 여부(비밀번호 인증)를 이 시점에 미리 지정한다. 이는 모니터·키보드 없이 SSH로만 접근하는 헤드리스 운용을 전제로 한 구성이다. 즉 부팅 후 처음부터 네트워크와 SSH가 열려 있어야 하므로, Wi-Fi 자격 증명과 SSH 활성화를 이미지에 미리 심는 것이 사실상 필수다. 마지막으로 USB에 기존 데이터가 있는지 확인한 뒤 쓰기를 진행하고, 완료되면 부팅 매체가 준비된다.

여기서 미리 설정한 hostname·SSH·Wi-Fi가 바로 다음 절의 접속 문제와 직접 연결된다.

Ubuntu Linux CLI 설치 실제 진행

1. Imager 실행

2. 디바이스 = Raspberry Pi 4 선택

3. OS = Other general-purpose OS → Ubuntu → Ubuntu Server 선택

4. 저장 장치(USB) 선택

5. hostname 설정 (home)

6. 지역·시간·키보드 레이아웃 설정

7. 사용자 이름·비밀번호 설정

8. Wi-Fi 설정 (핫스팟)

9. SSH 사용 설정 => 비밀번호 인증 방식

10. 최종 설정 확인 후 진행

11. USB 기존 저장 내용 확인 후 진행

12. 쓰기 진행

13. 완료

 

SSH 연결하기

mDNS(Multicast DNS)는 별도의 DNS 서버 없이 로컬 네트워크에서 호스트명.local 형태로 장치를 찾는 프로토콜(zeroconf)이다.

 

Raspberry Pi OS는 이 mDNS를 기본으로 지원하므로 ping 호스트명.local로 IP를 확인하거나 ssh 사용자명@호스트명.local로 곧바로 접속할 수 있었다.

 

문제는 Ubuntu Server에는 mDNS가 기본 탑재되지 않는다는 점이다. 따라서 Raspberry Pi OS에서 익숙하던 .local 접속이 그대로 통하지 않는다. 결국 접속 경로는 세 갈래로 갈린다.

flowchart LR
    A[Ubuntu Server 설치] --> B{mDNS 없음}
    B --> C[ip 명령으로 IP 직접 확인]
    B --> D[Netplan으로 네트워크 설정]
    B --> E[avahi 설치로 mDNS 복원]

아래 절들은 이 갈래를 각각 다룬다. Netplan은 네트워크 자체를 어떻게 구성하느냐의 문제이고, ip는 현재 상태를 진단·수정하는 도구이며, avahi 설치는 애초의 .local 편의를 되살리는 선택지다.

Netplan

Netplan은 우분투 17.10부터 네트워크 설정을 관리하기 위해 도입된 도구로, 기존의 /etc/network/interfaces 방식을 대체한다. 우분투 18.04 LTS부터는 네트워크 설정을 /etc/netplan/*.yaml 파일로 관리한다.

 

 

Netplan은 실제 설정을 직접 적용하지 않고 renderer라는 백엔드에 위임한다. renderer는 두 종류로 나뉜다.

  1. NetworkManager: 데스크탑 환경용. 이 값을 renderer로 두면 네트워크 인터페이스는 NetworkManager GUI를 통해 관리되며, Netplan 파일을 직접 편집하기보다 GUI로 설정한다.
  2. networkd: 주로 서버에서 사용되는 renderer.

이 구분은 앞 절의 선택과 맞물린다. GUI가 없는 Ubuntu Server에서는 NetworkManager의 GUI 경로를 쓸 수 없으므로 networkd 기반으로 YAML을 직접 작성하게 된다. 그리고 YAML은 들여쓰기로 구조를 표현하는 형식이므로, 들여쓰기가 어긋나면 그대로 오류가 된다. 

 

설정을 적용하는 명령은 상황에 따라 셋 중 하나를 택한다.

  • sudo netplan apply — 즉시·영구 적용하며 롤백이 없다.
  • sudo netplan try — 임시로 적용한 뒤 사용자가 확인하지 않으면 기본 120초 후 자동으로 이전 설정으로 되돌린다.
  • sudo netplan generate — 적용 없이 문법만 검증한다.

try의 자동 롤백은 헤드리스·원격 환경에서 특히 의미가 있다. SSH로만 접근하는 서버에서 잘못된 네트워크 설정을 apply로 굳히면 접속 자체가 끊겨 스스로를 잠가 버릴 수 있는데, try는 확인이 없으면 원상 복구되므로 이 위험을 줄인다.

Netplan 설정 파일 구조

설정 파일의 최상위 노드는 network이며, 그 아래에 version: 2(Curtin에서 사용하는 YAML 버전)와 renderer가 정의된다. renderer 값에 따라 ethernets:, wifis:, bridges: 같은 디바이스 정의 그룹이 추가되고, 각 디바이스 블록은 map 형태로 세부 항목을 기술한다. 즉 "network → (version, renderer) → 디바이스 그룹 → 개별 인터페이스"로 내려가는 계층 구조다.

 

우분투 18.04 네트워크 설정

아래는 18.04 기준으로 /etc/netplan의 설정을 작성하는 대표적인 패턴들이다.

설정 파일

데스크탑 버전을 설치한 경우 /etc/netplan의 기본 설정은 대개 다음과 같다.

ls /etc/netplan/*.yaml
# Let NetworkManager manage all devices on this system
network:
  version: 2
  renderer: NetworkManager

renderer가 NetworkManager로 지정되어 있으므로, 이 경우 네트워크 설정은 GUI를 통해 정의하는 것이 전제다. 서버 이미지에서 YAML을 직접 손대는 흐름과 대비되는 지점이다.

이더넷 DHCP 설정

DHCP 방식은 주소를 라우터로부터 자동 할당받는다.

network:
 version: 2
 renderer: networkd
 ethernets:
   wlan0:
     dhcp4: yes
     dhcp6: yes

dhcp4/dhcp6를 켜면 IPv4·IPv6 주소를 각각 자동으로 받고, sudo netplan apply로 적용한다. 다만 이 예시에는 유의할 불일치가 있다. 인터페이스 이름 wlan0은 통상 무선 인터페이스를 가리키는데 정작 ethernets:(유선) 그룹 아래에 정의되어 있다. 무선을 설정하려면 아래 "무선랜 설정"처럼 wifis: 그룹을 사용해야 하므로, 이 블록은 그룹과 인터페이스 종류가 어긋난 표기로 읽어야 한다.

이더넷 고정 IP 설정

고정 IP는 주소·게이트웨이·네임서버를 직접 지정한다.

network:
  version: 2
  renderer: networkd
  ethernets:
    wlan0:
      addresses:
        - 192.168.0.100/24
      gateway4: 192.168.0.1
      nameservers:
          addresses: [168.126.63.1,8.8.8.8]

여기서는 고정 IP로 192.168.0.100/24, 게이트웨이로 192.168.0.1, 네임서버로 168.126.63.1과 8.8.8.8을 지정한다. 한 가지 버전 의존적 주의점은 gateway4다. Ubuntu 22.04의 Netplan부터 gateway4(및 gateway6)는 deprecated 되어 경고를 출력하며, 기본 경로를 routes로 명시하는 방식이 권장된다. 구 문법도 당분간은 동작하지만, 대체 형태는 다음과 같다.

      routes:
        - to: default
          via: 192.168.0.1

이 변경은 게이트웨이를 특별 키로 두는 대신 일반적인 라우팅 규칙(기본 경로)으로 통일하려는 방향으로 이해할 수 있다.

무선랜 설정 (이걸 많이 사용)

무선은 wifis: 그룹 아래 접속할 AP 정보를 기술한다.

network:
  version: 2
  renderer: networkd
  wifis:
    wlan0:
      dhcp4: yes
      optional: true
      access-points:
        "network_ssid_name":
          password: "**********"

주소는 DHCP로 할당받고, access-points 아래에 SSID와 패스워드를 넣어 접속 대상 AP를 지정한다. 헤드리스 Raspberry Pi는 유선을 항상 끌어오기 어려운 만큼 실제로 이 무선 설정을 가장 많이 쓰게 된다. 앞의 DHCP 예시와 비교하면, 같은 wlan0이라도 무선은 ethernets:가 아니라 wifis: 그룹에 두어야 한다는 점이 분명해진다.

IP 확인 및 수정

네트워크 상태의 확인과 수정은 ip 명령으로 한다. ip는 다음과 같이 대상(OBJECT)과 동작(COMMAND)을 조합하는 구조다.

ip [ OPTIONS ] OBJECT { COMMAND | help }

주요 OBJECT는 다음과 같다.

OBJECT 의미

link 모든 네트워크 인터페이스의 상태 관리·출력
address IP 주소와 그 특징 정보 출력
route 라우팅 테이블 변경·출력
maddr 멀티캐스트 IP 주소 관리·출력
neigh 이웃 객체 출력, ARP 테이블 출력

이 대상들에 확인(show)이나 추가/삭제(add/del), 활성화/비활성화(up/down) 같은 동작을 붙여 사용한다. 아래 하위 항목들이 그 조합이다.

address Object로 ip 세부사항 확인하기

address 대상으로 IP 세부사항을 조회한다. 다음은 모두 같은 동작의 축약형이다.

ip a
ip addr
ip address
ip address show
ip a show

mDNS가 없는 Ubuntu Server에서 장치의 IP를 확인하는 1차 수단이 이 명령이다.

address로 ip 주소 추가하고 삭제하기

같은 address 대상에 add/del을 붙여 주소를 직접 추가·삭제할 수 있다.

ip addr add 123.123.5.10/24 dev eth0
$ ip addr del 123.123.5.10/24 dev eth0

dev <인터페이스>로 대상 인터페이스를 지정한다. 다만 ip 명령으로 붙인 주소는 런타임 설정이므로, 영구적으로 유지하려면 앞의 Netplan 설정으로 기술하는 것이 정공법이다.

link 명령어로 세부사항 확인하기

link 대상은 인터페이스 자체의 상태를 다룬다. 확인은 다음 축약형들로 한다.

ip l
ip li
ip lin
ip link
ip link show

link로 인터페이스 활성화 / 비활성화 시키기

link set에 up/down을 주어 인터페이스를 켜고 끈다.

ip link set eth0 down
ip link set eth0 up

route로 라우팅 정보를 출력하기

route 대상으로 라우팅 테이블을 확인한다.

ip route show

앞서 Netplan에서 지정한 기본 경로(게이트웨이)가 실제로 반영됐는지 확인하는 지점이기도 하다.

maddr 이용해서 ip 주소 출력하기

maddr 대상으로 관리 가능한 멀티캐스트 주소를 모두 출력한다.

ip maddr show 

maddr로 멀티캐스트 주소 추가하고 삭제하기

멀티캐스트 주소도 add/del로 관리한다.

ip maddr add 33:33:00:00:00:01 dev eth0
ip maddr del 33:33:00:00:00:01 dev eth0

 

기존의 HostName.local을 사용하고 싶다면?

마지막 선택지는 문제의 출발점으로 돌아간다. .local 접속의 편의 자체를 되살리려면 Ubuntu Server에 mDNS를 직접 설치하면 된다. avahi-daemon과 libnss-mdns가 mDNS/zeroconf를 통해 .local 이름 해석을 제공한다.

sudo apt update
sudo apt install -y avahi-daemon libnss-mdns
sudo systemctl enable --now avahi-daemon

설치 후 서비스를 활성화하면 ping 호스트명.local로 응답을 확인할 수 있고, ssh 사용자명@호스트명.local로 IP를 몰라도 곧바로 접속할 수 있다. 결국 이 절은 Raspberry Pi OS가 기본 제공하던 mDNS를 Ubuntu Server에 수동으로 얹어, 앞에서 끊겼던 .local 접속 흐름을 원래대로 복원하는 과정이다.

master UTF-8 HTML HIBACHI Ln 1, Col 1