File4developers
- 여기서는 리눅스의 파일과 디렉토리를 살펴보고, 그 파일과 디렉토리를 방법에 대해 알아보겠다. 파일 만들기, 열기, 읽기, 쓰기, 그리고 닫기에 대해 알려줄것이며, 예제를 통해 디렉토리를 생성하고 찾고 지우는 일련의 조작방법도 알아볼거다.
근데 나도 잘 몰라 - 리눅스 파일 IO 다루는 방법 알아보기 전에 파일, 디렉토리, 장치에 관련된 개념을 좀 알아볼거다. 파일과 디렉토리를 조작하기 Win32API같은 System Call을 사용할 필요가 있는데, 라이브러리 함수가 있다
귀찮으신 분들이 다 만들어 두셨다 만세
Linux / Unix에서 파일의 구조
- "왜 파일구조를 알아야 함? 나 윈도우에서도 파일 존나 열고 닫아봤어 다 안다고"
일단 명치 존나 쎄게 한대만 맞고 시작하면 안될까?일단 좀 다르다.이런 씨댕 다 알면 그냥 안 읽으면 되잖아 - Linux에서는 일반적으로 어플리케이션들은 디스크 파일, 시리얼 포트, 프린터, 기타 디바이스까지 파일로 쓴다. 여기서 예외적인게 몇가지가 있긴 한데 일단 신경쓰지말고.. 가장 중요한게 열기, 닫기, 읽기, 쓰기, ioctl이다.
- 디렉토리 역시 파일의 한 종류고, 존나 옛날 SCO Unix 초기버전 같은거에서는 슈퍼유저도 디렉토리를 못 지웠다고 한다. 우린 그냥 지우면 된다.
요즘 리눅스는 가끔 윈도우보다 편할때도 있다니깐 아 물론 가끔임
디렉토리
- 일단 윈도우에서는 폴더라는 이름으로 부른다.
- 파일은 그 이름과 관리정보, 그러니까 언제 만들어졌는지, 권한은 뭐가 필요한지 같은 정보를 가지고 있다. 관리정보는 inode에 저장되고, inode는 파일의 크기와 디스크상에 물리적으로 저장된 위치정보를 담고있다. 시스템은 파일의 inode 번호를 사용한다.
- 디렉토리는 inode와 다른 파일들의 명칭을 저장하고 있는 파일이다. (디렉토리 파일 자체를 볼 수 있는 방법을 아는 사람은 추가바람)
- 파일은 디렉토리에 정렬되며 보조 디렉토리를 포함할 수도 있다. 이러한 형태는 CP/M 시절부터도 써오던거다. drake라는 사용자는 보통 그의 홈디렉토리 /home/drake에 전자메일, 업무용 문서, 만든 프로그램 등을 저장하는 보조 디렉토리가 있을거다. 리눅스는 현재 사용자의 홈 디렉토리를 ~로 표시하는 존나 훌륭한 표기법을 가지고 있다.
귀차니즘의 승리다른 사용자의 홈 디렉토리는 ~user로 표기된다. 예를들어 disint라는 사용자의 디렉토리는 ~disint이다. 일반적으로 각 사용자의 홈 디렉토리는 /home의 하위 디렉토리다. - /home 디렉토리는 /라는 루트 디렉토리의 서브 디렉토리다. /bin에는 시스템 프로그램이 저장돼 있고, /etc에는 시스템 환경설정파일이, /lib에는 시스템 라이브러리가 모여있다. 물리적인 장치를 나타내는 파일과 이러한 장치에 대한 인터페이스를 제공하는 것들은 /dev, /proc, /sys 에 있다.
파일과 장치
- 하드웨어 장치도 유닉스에서는 파일로 표현된다.
- 예를 들어 /mnt/cdrom으로 CDROM을
그라운딩마운트하려면 다음과 같다.GUI를 쓰면 그냥 인식된다. 즉 뻘짓임
<source lang="bash"> $ mount -t iso9660 /dev/sdc /mnt/cdrom $ cd /mnt/cdrom </source>
- 위의 내용은, 일단 부팅될때 cdrom을 찾으면 어디라고 나온다. 그게 /dev/sdc이었던거다. 그걸 /mnt/cdrom에다가 마운트한다는 의미다.
/dev/console
- 이 장치는 시스템 콘솔이다. 에러메세지나 시스템 및 운영체제의 메세지는 이쪽으로 표시가 된다. 일반적으로 tty1~8, 시리얼포트쪽으로 뽑아낼 수 있다. 커널 설정에서 바꿔줄 수 있는데, 이 방법에 대해서는 다른 문서를 참조. 아니면 간결히 설명해주실 분이 추가바람
/dev/tty
- 특별한 파일 /dev/tty는 닉네임이다. 현재 제어가 가능한 터미널을 지칭한다.
- /dev/console과 다른점은, /dev/console은 고정되어 있고, /dev/tty는 바뀔 수 있다는 것.
/dev/null
- 의미없는 장치.
가끔 잉여로운 생활이 필요할때도 있지 - 이 장치로 보내지는 모든 출력(output)은 무시된다.
꼰대처럼 - 이걸 읽으면 파일의 끝문자인 EOF가 반환되는데, 이는 cp명령을 사용할때 빈파일로 사용될 수 있다.
- 일반적으로 디버그용의 문자를 출력하고 디버그가 종료됐을 때, 그냥 출력을 이쪽으로 돌리는 것을 많이 볼 수 있다.
<source lang="bash"> $ echo do not want to see this > /dev/null $ cp /dev/null empty_file </source>
- /dev안에 다른 장치들은 하드디스크나 플로피디스크, 통신 포트, 테이프 드라이브, CDROM, 사운드칩, 그리고 몇몇 시스템의 내부적 상태를 나타내는 장치들로 구성된다. 용량이 0인 파일을 생성하기 위해 null바이트의 원천으로 작동하는 /dev/zero라는 장치도 있다. 대부분은 루트권한이 필요하다. 일반 사용자가 하드디스크 저수준 입출력을 가능하게 한다면..
헬게이트 오픈
시스템 호출과 장치 드라이버
- 몇가지 함수로 파일과 장치를 컨트롤하거나 읽고 쓸 수 있다. 이런 함수를 시스템 콜이라고 부르며, 이것은 리눅스에 의해 직접 제공되고, 그 자체로 OS에 대한 인터페이스라고 볼 수 있다.
- OS의 심장 커널은 많은 장치 드라이버를 갖고 있다. 이 드라이버는 시스템의 하드웨어를 제어할 수 있는 저수준 인터페이스 모음이다. 예를들어, 테이프 드라이브의 장치드라이버는 테이프 장치를 켜고 앞으로 감거나 뒤로 감거나 테이프에 직접 읽고 쓰는 방법이 있어야 테이프 장치를 쓸 수 있을거 아닌가. 또, 드라이버는 장치의 버퍼 등을 최대한 활용할 수 있게 만들어져 있다.
- 하드디스크 드라이버 역시 사용자가 몰라도 되는, 섹터, 클러스터에 대한 접근을 할 수 있다. 랜덤억세스 역시 마찬가지. 변태가 아닌 일반 프로그래머가 '나 랜덤억세스로 할래'라고 하지 '랜덤억세스를 어떻게 하냐면..' 하고 프로그램을 짜진 않는다.
- Read, Write가 아닌, 독특한 세팅은 ioctl() 함수를 이용해서 억세스하는게 일반적이다.
- /dev/ 안의 디바이스파일도 그냥 일반 파일처럼 읽고 쓰는거다.
- 장치 드라이버를 제어할 수 있는 저수준 함수에는 다음과 같은 것들이 있다.
- open - 파일 / 장치 열기
- read - 열린 파일 / 장치로부터 읽기
- write - 열린 파일 / 장치에 쓰기
- close - 파일 / 장치 닫기
- ioctl - 특별한 제어
- ioctl이라는 시스템 호출은 필수적인 하드웨어 제어 기능을 제공한다. 시리얼포트같은 경우 read/write만 가능하면 어떻게 세팅을 하겠는가.
존나 옛날에는 AT 어쩌구로 제어하긴 했었다 ATDT01410같이.. - 시스템 콜이나 POSIX는 리눅스 맨페이지 2섹션에 있다.
물론 영어거기에 보면 시스템 콜에 대한 짤막한 설명이 있다.
라이브러리 함수
- 입출력에 사용되는 저수준 시스템 콜을 사용할때 문제는 시스템 콜이 존내 비효율적이라는데 있다.
- 시스템콜을 사용하는것은 수행 측면에서 별로 안 좋다. 리눅스는 어플리케이션 코드를 실행하다가 모드 변경해서 커널 코드를 수행하고 다시 모드 변경해서 어플리케이션을 돌리는 방식이다. 시스템 콜은 그냥 함수 쓰는거에 비해 존나 느리다.
- 하드웨어는 버퍼가 있는데, 시스템 콜로 읽고 쓰면 버퍼를 안 거친다.
물론 하드웨어에 대해 빠삭하게 알고 접근하면 이야기가 좀 다르긴 한데, 님 잘 알음?
- 이에 비해 장치나 디스크에 효율적으로 입출력을 하기 위해 만들어진 표준 라이브러리가 있다. 라이브러리는 효율적으로 입출력을 하기 위해 만들어져 있으니, 시스템 콜을 최소화할 수 있다. 물론 표준 라이브러리 내부에서 시스템 콜을 쓰긴 하지만, 최대한 느려지지 않게 잘 만들어져 있다.
- 라이브러리 함수는 리눅스 맨 페이지 3섹션에 있다. 보통 그와관련된 표준 include파일(헤더)를 갖는데, 표준 입출력 라이브러리에 대해서는 stdio.h같은 헤더파일이 있다.
저수준 파일 입출력
- 각각 실행되는 프로그램을 프로세스라 부르는데, 이 프로세스는 많은 File Descriptors와 관련된다. 이건 열린 파일이나 장치들을 쓰기 위해 사용할 수 있는 작은 정수값이다. 이 값이 얼마나 많이 있는가는 유닉스 시스템이 어떻게 설계되어 있는가에 달려있다. 프로그램이 시작될 때, 보통 이미 열려져 있다면 이 세가지 중 하나의 값을 가진다.
- 0 - 표준 입력
- 1 - 표준 출력
- 2 - 표준 에러
뽸
- open이라는 시스템 콜을 사용하면 파일이나 장치에 대해 다른 File Descriptor를 연결시킬 수 있다. 근데 자동으로 열려진 File Descriptor에 write를 사용하여 몇가지 단순한 프로그램을 생성할 수 있다.
Write
<source lang="c"> /* write */
- include <unistd.h>
size_t write(int fildes, const void *buf, size_t nbytes); </source>
- write 시스템 콜은 filedes라는 File Descriptor와 관계된 파일에 쓰기 위해 buf로부터 첫 nbytes만큼의 바이트 수를 읽는다. 이건 진짜 write할 바이트의 수를 되돌려준다. File Descriptor에 에러가 있었다거나 write하려는 디바이스 드라이버가 블록 크기에 상관이 있다면 nbytes 이하가 될 수도 있다. 함수가 0을 반환하면, 파일의 끝(EOF)에 도달했다는 것이고, -1이라면 에러가 있었다는 것이며, 그 에러가 뭔 지 볼 수 있다.
<source lang="c"> /* simple_write.c */
- include <unistd.h>
int main() {
if ((write(1, "Here is some data\n", 18)) != 18) write(2, "A write error has occurred on file descripter 1\n", 47); return 0;
}
- 이 프로그램은 단순히 표준 출력(콘솔)로 메세지를 낸다. 프로그램이 종료될 때 모든 열려있는 File Descriptor는 자동으로 닫히니까, 우린 파일을 닫아야 할 필요는 없지만, 안 쓰면 닫는게 좋다. 버퍼링된 출력을 할땐 걍 열어놓고 종료하면 망한다.
<source lang="bash"> $ sudo ./simple_write Here is some data $ </source>
Read
<source lang="c"> /* read */
- include <unistd.h>
size_t read(int fildes, void *buf, size_t nbytes); </source>
- read 시스템콜은 File Descriptor랑 연결된 파일로부터 nbytes보다 큰 buf에 nbytes만큼의 크기로 읽어온다. 이 함수는 읽은 데이터의 바이트수를 반환하는데, 반환값이 nbytes보다 작을수도 있다. 만약 read 함수가 0을 반환하면, 읽을게 없다는 얘기다. 호출할때 에러가 발생하면 -1을 반환한다.
- 이 프로그램은 simple_read.c라는 프로그램인데, 표준 입력에서 처음 128바이트를 읽어서 표준 출력으로 뿌려주는 프로그램이다. 입력이 128바이트 이하면 입력된 만큼만 뿌린다.
<source lang="c"> /* simple_read.c */
- include <unistd.h>
int main() {
char buffer[128]; int nread;
nread = read(0, buffer, 120); if (nread == -1) write(2, "A read error has occurred\n", 27); if ((write(1, buffer, nread)) != nread) write(2, "A write error has occurred\n", 28); return 0;
} </source>
- 프로그램을 실행하면 다음과 같은 결과를 볼 수 있다.
<source lang="bash"> $ echo hello there | ./simple_read hello there $ ./simple_read < draft1.txt draft1.txt$ </source>
open
- 새로 File Descriptor를 만들려면 open이라는 시스템 콜을 사용해야 한다. creat라는 호출은 POSIX 표준이고, 리눅스에서 지원된다. 근데 요샌 잘 안 쓴다. 안 쓰는 이유는 앞에서 설명했음.
<source lang="c"> /* open */
- include <fcntl.h>
- include <sys/types.h>
- include <sys/stat.h>
int open(const char *path, int oflags); int open(const char *path, int oflags, mode_t mode); </source>
- open은 파일이나 장치에 경로를 만드는거다. 성공하면 read나 write에서 쓰게 될 File Descriptor를 반환한다. File Descriptor는 열때마다 생기는데, 프로그램 두개에서 파일 하나를 같이 쓸 수 있다. 근데 동시 입출력이 read면 그나마 괜찮은데, write를 두군데서 하면..
더이상의 자세한 설명은 생략한다 - path는 파일이름이고, oflags는 파일 모드다.
- oflags는 다음 중 하나를 골라서 쓰면 된다.
- O_RDONLY - 읽기전용
- O_WRONLY - 쓰기전용
- O_RDWR - 읽고쓰기용
- 일단 모드 선택을 하고 | 연산으로 추가 옵션을 줄 수 있다.
- O_APPEND - 파일이 있으면 지우지 않고, 뒤에다가 추가한다.
- O_TRUNC - 파일이 있으면 일단 지우고 시작한다.
본격 조때바라 플레이 - O_CREAT - 파일이 없으면 만든다.
- O_EXCL - O_CREAT랑 같이 사용하여 파일을 만드는데, 이미 파일이 있다면 에러를 뱉는다.
- 몇가지 옵션이 더 있는데 man 2 open 으로 확인할 수 있다.
- open이 제대로 되면, File Descriptor를 양수(+)로 반환하고, 실패하면 -1을 반환한다. 실패시 errno라는 전역변수를 설정한다.
Permission
- open에서 O_CREAT로 파일을 생성할 때 권한을 줄 수 있다. mode는 sys/stat.h에 정의되어 있다. | 연산자로 OR해서 쓴다.
- S_IRUSR - 소유자에게 읽기 권한
- S_IWUSR - 소유자에게 쓰기 권한
- S_IXUSR - 소유자에게 실행 권한
- S_IRGRP - 소유그룹에게 읽기 권한
- S_IWGRP - 소유그룹에게 쓰기 권한
- S_IXGRP - 소유그룹에게 실행 권한
- S_IROTH - 그외에게 읽기 권한
- S_IWOTH - 그외에게 쓰기 권한
- S_IXOTH - 그외에게 실행 권한
- 예를들어
<source lang="c">open("myfile", O_CREAT, S_IRUSR|S_IXOTH);</source> 라고 해주었을 경우, myfile이란 이름의 파일을 만들면서 소유자에게 읽기 권한, 나머지는 실행만 할 수 있게 만들어진다. <source lang="bash"> $ ls -la myfile 0 -r-------x 1 drake ubuntu 0 Jun 09 23:55 myfile* </source>
umask
- umask는 좀더 친숙한 숫자로 퍼미션을 정하는 함수다.
- umask 명령어와 같은 방식으로 사용하면 된다. 즉, chmod로 777이면 rwxrwxrwx지만, umask는 000이 rwxrwxrwx이다.
- 이 내용은 추후 추가
close
<source lang="c"> /* close */
- include <unistd.h>
int close(int fildes); </source>
- File Descriptor랑 파일 사이의 연결을 종료하기 위해 close를 사용할 수 있다. File Descriptor는 나중에 다시 쓸 수 있다. 성공은 0, 에러는 -1
- 수행중인 프로그램 하나가 동시에 열 수 있는 파일의 수는 디바이스마다 다르지만, bits/stdio_lim.h내에 define된 FOPEN_MAX가 있다. 기본값은 16이다.
ioctl
<source lang="c"> /* ioctl */
- include <unistd.h>
int ioctl(int fildes, int cmd, ...); </source>
- ioctl은 디바이스 드라이버가 정의한대로의 방식으로 쓰인다.
- 사용법은 각 디바이스 드라이버별로 다르므로 여기엔 안 적겠다.
개물님들 도와주셈