유닉스에서 사용되는 proc파일 시스템은 운영체제의 각종 정보를 커널모드가 아닌 유저모드에서 쉽게 접근할 수 있도록 만들어 줌으로 시스템 정보를 일반 프로그래머가 쉽게 접근 할 수 있도록 도와준다.
특히 리눅스에서는 프로세스 정보뿐만 아닌 다른 시스템 정보들까지 광범위 하게 제공해 준다. 이말은 proc파일시스템을 제대로 이해할 경우 리눅스 운영체제를 좀더 깊이 있게 다룰 수 있다는 말이 된다. 실제 ps와 같은 프로세스 상황감시에서 부터, CPU사용율, 인터럽트, 네트워크 패킷전송량, 적재된 모듈, IDE-SCSI와 같은 장치정보, CPU정보등의 데이터를 어렵지 않게 얻어 올 수 있다. 다른 대부분의 유닉스에서 이러한 정보를 얻어올려면 상당한 애로사항을 격게 될것이다.
이제 proc파일시스템에서 데이터를 읽어오는 것을 지나서 proc파일 시스템에 필요한 데이터를 쓰는 방법에 대해서 알아보도록 하겠다.
우리는 이미 일반 파일 시스템을 이용해서 필요한 데이터를 남기는 방법을 알고 있다. read(2), open(2), write(2) 이 3개의 함수만 사용할 줄 안다면, 필요한 모든 데이터를 읽고 쓰는데 별 부족함이 없다. 그렇다면 왜 굳이 proc파일시스템을 이용해야 하는지에 대해서 알아 보도록 하겠다.
일반적으로 사용되는 파일 시스템은 상당한 오버헤드를 가지고 있다. 각 파일의 inode와 superblocks와 같은 객체를 관리해야 하며 이러한 정보를 필요할때 마다 운영체제에 요청해야 한다. 이들 파일 시스템의 데이터들은 서로 어긋날수도 있으며, 단현화 현상등이 발생할 수도 있다. 운영체제는 이러한 모든 것을 관리해 주어야 하며, 당연히 상당한 오버헤드가 발생하게 된다.
proc파일 시스템은 이러한 일반 파일시스템의 문제점을 없애기 위해서 리눅스 커널에서 직접 파일시스템을 관리하는 방법을 채택하고 있다.
지금 여러분의 리눅스 시스템에서 mount명령을 내리면 다음과 같이 proc 파일 시스템이 자동으로 마운트 되어 있는 것을 확인할 수 있을 것이다.
# mount
/dev/hda7 on / type ext3 (rw)
none on /proc type proc (rw)
/dev/hda5 on /usr type ext3 (rw)
...
여러분이 최초에 리눅스 운영체제를 설치할 때 proc파일 시스템을 위해서 별도로 파티션작업을 한적이 없을 것이므로 mount정보에 표시되는게 이상할 수도 있을 것이다. 이유는 앞에서 말했듯이 proc파일 시스템은 리눅스 커널에서 직접 관리하는 것으로 운영체제가 부팅 되었을 때 생성되는 파일 시스템이기 때문이다. mount정보를 보면 알겠지만 어떤 장치에도 마운트되어 있지 않음을 확인할 수 있다. proc파일 시스템은 커널메모리에서 돌아가는 일종의 가상 파일 시스템이다.
일반적인 파일 시스템 계층은 은 프로그래머를 위해서 POSIX 형식의 인터페이스를 제공한다. open, read, write, close등이 이것이다. 데이터 블럭들은 용이한 확장을 위해서 추상화 되어 있으며 상속가능한 형태로 구성된다.
이러한 일반 파일 시스템은 대용량의 데이터를 다루어야 하는 경우 매우 유용하지만, 고정적이고 처리해야할 데이터의 양이 적은 분야에는 오히려 비효율적이다. proc파일 시스템에서 다루어야 할 정보는 대부분 정해져 있으며, 데이터의 양도 그리 많지 않다. 고로 일반 파일시스템에서 제공하는 인터페이스를 사용하지 않고 필요한 작업에 최적화된 인터페이스를 사용할 수 있다.
proc파일 시스템(이하 proc) 자체가 커널과 밀접하게 연관있는 이유로 일반 애플리케이션에서 proc를 사용하는 일은 드물다. 위에서 설명 했듯이 커널메모리에서 proc를 유지하게 되므로 많은 양의 데이터를 처리하는 애플리케이션의 용도와는 맞지 않다는 점도 있다.
그런점에서 proc는 커널모듈과 같이 커널과 밀접하게 관계있는 프로그램에서 유용하게 사용할 수 있다. 커널 모듈 프로그램은 주로 장치를 올리기 위한 용도로 사용되는데, 커널 레벨에서 작동하다 보니 모듈의 작동상황이나 성능등을 알아오기가 그리 쉽지 않다. 그렇다고 해서 일반 파일 시스템을 IPC를 사용하는 것 역시 그리 좋은 생각은 아니다. 이럴 때 proc를 이용하면 문제를 깔끔하게 해결 할 수 있다.
name은 proc파일의 이름이다. mode는 proc파일의 권한으로 일반파일에 사용되는 권한과 동일하게 사용할 수 있다. mode에 대한 자세한 내용은 stat(2)의 man페이지를 참고하기 바란다.
struct proc_dir_entry *next ...는 proc파일이 위치하는 디렉토리로 일반 파일에서의 디렉토리 권한과 동일하게 사용되며, 링크드 리스트로 관리된다.
data proc에서 읽은 데이터를 리턴하기 위해서 사용된다.
read_proc, write_proc 유저영역의 프로세스는 직접 커널영역에 데이터를 읽거나 쓸수 없다. 때문에 모듈 프로그램등이 중간에서 커널과 유저영역 사이의 데이터전달을 해주어야 한다. 이러한 데이터 전달은 callback함수를 통해서 이루어진다. read_proc는 커널로 부터 읽은 데이터를 유저영역 프로세스로 되돌려주기 위해서 write_proc는 유저역역 프로세스에서 쓴데이터를 커널메모리 영역으로 복사하기 위해서 사용한다.
proc는 사용하기 간단한 몇개의 API만을 제공하는데, 이들 API는 커널의 메이저 버젼에 따라서 차이가 있을 수 있다. 만약 여러분이 2.4.x외의 다른 커널 버젼을 사용하길 원한다면 해당 커널버젼의 커널 문서를 참고해야 할 것이다. 그렇다고 해서 이 문서가 전혀 필요 없지는 않을 것이다. 대부분의 경우 커널의 메이저 버젼이 업그레이드 된다고 하더라도 함수 API가 아주 크게 변하는 경우는 없기 때문이다. 이 문서를 익혀 놓는다면 다른 커널 버젼에도 쉽게 적응할 수 있을 것이다.
일반 파일에서 유저와의 데이터 교환은 매우 단순하며, 별로 신경쓸 필요도 없다. 프로그램이 파일에 쓴 내용 그대로를 유저가 보며, 유저가 파일에 쓴 내용그대로를 다시 프로그램이 읽어들인다.
그러나 proc파일 시스템에서의 데이터는 실제 파일에 저장되는 것과는 달리 커널메모리에 저장된다. 알다 시피 커널메모리는 유저레벨 에서 직접 접근할 수 없다. 유저가 cat(혹은 read함수)등을 통해서 파일의 내용을 읽을려고 하면 커널에서 데이터를 유저에게 일정한 포맷으로 뿌려주게 된다. 마찬가지로 유저가 어떤 내용을 proc파일에 쓰게되면 데이터를 받아들인후 가공해서 커널메모리에 적재하게 된다.
이를 위해서 커널과 일반유저 사이에 데이터를 서로에게 전달해 주는 어떤 함수가 필요하고 이 함수가 다룰 수 있는 표준적인 자료구조가 있어야 한다. 유저가 데이터를 읽고 쓰기 위해서는 읽기와 쓰기를 위한 callback함수를 등록시켜서 사용 해야한다.
그럼 간단한 예제를 만들어 보도록 하겠다. 예제는 일반 애플리케이션이 아닌 커널 모듈 프로그램이다. 커널 모듈프로그래밍에 대한 내용은 커널 모듈 프로그래밍을 참고하기 바란다.
프로그램의 이름은 my_proc.c로 하겠다. 이 모듈은 proc파일 시스템에 myproc라는 디렉토리를 만들고 이 모듈 아래에 foo와 jiffies라는 파일을 만든다. 만약 유저가 cat등을 통해서 이들 파일을 열면 모듈은 모듈 정보를 적당한 포맷으로 만들어서 사용자에게 보여주게 된다. foo파일의 경우에는 파일에 내용을 쓸수도 있도록 되어 있어서 사용자가 내용을 바꾸면 이 내용은 모듈에서 읽어 들이게 된다.
여러분이 커널 모듈 프로그래밍에 대한 이해가 있다는 가정하에 설명은 주석으로 대신하도록 하겠다. 모듈에서 main()함수에 해당하는 module_init()함수에서 차근차근 분석해나가면 좀더 쉽게 이해할 수 있을 것이다.
// 사용자가 cat등을 통해서 /proc/[MODULE_NAME]/jiffies파일을 열면
// 커널은 이 함수를 호출해서 해당 정보를 넘겨준다.
static int proc_read_jiffies(char *page, char **start, off_t off,
int count, int *eof, void *data)
{
int len;
MOD_INC_USE_COUNT;
// 사용자가 이해하기 쉬운 포맷으로 만든다.
// cat, vi등을 사용해서 이 파일을 열경우
// jiffies = 1234 와 같은 형식으로 보인다.
len = sprintf(page, "jiffies = %ld\n", jiffies);
// 해당 내용을 printk를 통해서 로그로 남긴다.
// 이 데이터는 /var/log/message로 출력된다.
printk("<1> read jiffies = %ld\n", jiffies);
MOD_DEC_USE_COUNT;
return len;
}
// 사용자가 /proc/[MODULE_NAME]/foo 파일을 열었을 때
// 출력해주는 정보
static int proc_read_foobar(char *page, char **start, off_t off,
int count, int *eof, void *data)
{
int len;
struct fb_data_t *fb_data = (struct fb_data_t *)data;
MOD_INC_USE_COUNT;
// fb_data구조체의 내용을 보시쉽게 만들어서 출력해준다.
len = sprintf(page, "%s = %s",
fb_data->name, fb_data->value);
MOD_DEC_USE_COUNT;
return len;
}
// 사용자는 /proc/[MODULE_NAME]/foo에 내용을 쓰기를 원할때도 있을 것이다.
// 이 때 이함수가 호출된다.
static int proc_write_foobar(struct file *foke,
const char *buffer,unsigned long count, void *data)
{
int len;
struct fb_data_t *fb_data = (struct fb_data_t *)data;
MOD_INC_USE_COUNT;
if (count > FOOBAR_LEN)
len = FOOBAR_LEN;
else
len = count;
printk("<1> DATA COPY %d\n", len);
// echo "xxxxx" > /proc/[MODULE_NAME]/foo 등으로 입력받은 값을
// fb_data->value에 저장한다.
if (copy_from_user(fb_data->value, buffer, len))
{
MOD_DEC_USE_COUNT;
return -EFAULT;
}
fb_data->value[len] = 0x00;
MOD_DEC_USE_COUNT;
return len;
}
// 커널 모듈 초기화 함수
static int init_myproc(void)
{
int rv = 0;
// JIFFILE파일의 경우 단지 읽기만 허용한다.
example_dir->owner = THIS_MODULE;
jiffies_file = create_proc_read_entry(JIFFIE_FILE, 0444,
example_dir, proc_read_jiffies, NULL);
if (jiffies_file == NULL)
{
rv = -ENOMEM; printk("<1> read entry failure\n");
goto no_jiffies;
} printk("<1> OK MAKE MODULE\n");
jiffies_file->owner = THIS_MODULE;
// FOO_FILE의 경우 읽기와 쓰기 모두 가능해도록 해야 하며
// 각각의 경우에 호출될 함수를 지정해 줘야 한다.
foo_file = create_proc_entry(FOO_FILE, 0644, example_dir);
if (foo_file == NULL)
{
rv = -ENOMEM;
goto no_foo;
}