시작
리눅스 시스템에서 메모리는 커널 영역과 유저 영역으로 분리되어 있습니다. (논리적으로..)
그 이유는 OS가 관리하고 있는 영역을 사용자의 응용 프로그램이 임의로 사용(Read,Write)하면,
시스템에 치명적인 문제가 생길수 있기 때문입니다. 그래서 두 영역으로 분리 했습니다.
위와 같은 이유로 사용자는 함부로 커널 영역에서 입출력을 할 수 없습니다.
전에 응용이 어떻게 디바이스 드라이버를 다루는가 글을 올렸습니다. 응용 프로그램이 어떻게 커널 영역에 있는
디바이스 드라이버를 다루는가? 바로 chrdevs[주번호] 또는 blkdevs[주번호] 자리에 저장되어 있는,
저수준 입출력 함수(open,read,write,lseek..)와 실제 디바이스를 작동시키는함수들을 1:1 매칭 시켜놓은
file operation 구조체를 이용하여 장치를 다루게 됩니다.
이 글에서는 사용자가 호출하는 입출력 함수가 아닌 디바이스 드라이버의 기본적인 입출력 함수 구현을 적어 보겠습니다.
(공부한 내용을 정리한 것이며 당연 틀린 내용이 있을 수 있습니다.)
file operation 구조체
1 | struct file_operations ???_fops ={ |
C로 만들어진 디바이스 드라이버 소스 파일에 struct file_operations 로 시작하는 구조체 부분이 있을 겁니다.
이 부분이 응용이 호출 하는 일반 함수들과 실제 장치들의 동작을 제어하는 함수를 1:1 연결 시켜주는 부분입니다.
???에는 임의의 이름을 적어 주시면 됩니다. 되도록 장치 이름을 간략화해서 사용 하는게 가독성에서 조금 더 좋습니다.
저수준 함수말고 다른 필요 함수가 있다면 ioctl이라는 것을 이용하여 따로 구현해 주면 됩니다.
사용 예를 들면 응용이 open을 호출하면 실제는 연결된 ???_open이 호출됩니다.
open
디바이스 드라이버의 open은 일반 저수준 입출력 함수인 open()처럼 fd값을 리턴하는게 아닌,
장치를 사용하기 위한 초기화와 몇명의 사용자가 장치를 사용하고 있는지에 대한 정보를 생성,갱신합니다.
1 | int ???_open(struct inode* inode, struct filp* filp) |
위와 같은 형태이며 인자로 파일에대한 정보, 메타데이터를 가지고 있는 inode라는 파일과 파일의 연산을
처리하기 위한 file pointer 구조체 filp을 받습니다. (f_flags, f_pos,f_op.. 마지막에 설명하겠습니다.)
일반 open 함수가 정상 수행되면 3이상의 값을 반환 하는것에 비해, 디바이스 드라이버의 open은 정상 수행시 0
을 반환하고 에러시 음수를 반환합니다.
응용은 이미 디바이스 드라이버에 접근하기 위한 주번호를 가지고 있기 때문에 응용이 호출한 open의 반환값(fd)
은 무시해도 됩니다. 반환값이 0이면 정상적으로 수행 된 것.
/ open시 초기화할 내용/ 부분에 실제 H/W들을 다루기 위한 코드들을 적어주면 디바이스 드라이버의 open이
구현됩니다.
그리고 open시 부번호를 이용하여 각각 연결되는 fop를 다르게 할 수도 있습니다.
read
디바이스 드라이버의 read는H/W의 상태나 특정 위치 값을 읽어오기 위해 사용합니다.
코드 내용으로는 데이타시트를 보고 H/W의 제어 함수를 작성 해줘야 하고, 위에 처음에 적었듯이
사용자영역에서 커널 영역으로 혹은 그 반대의 경우도 입출력이 제한 되있습니다.
하지만 커널 함수들을 이용하면 입출력이 가능합니다. copy_to_user()와 put_user()입니다.
1 | ssize_t ???_read(struct file *filp, char *buf, size_t count, loff *f_pos) |
read함수의 구성은 위와 비슷합니다.
인자부터 보면 , *filp는 파일의 연산을 다룰 정보를 가진 구조체, 일반 open()호출시 반환되는 fd입니다.
buf는 장치로부터 읽어서 잠시 저장할 장소, count는 얼마나 읽어올 것인지 (byte단위), f_pos는 lseek()에 사용
되는 파일 포인터 위치(오프셋)입니다. 맨 처음 장치의 읽어올 값이 있는지 검사하고 open으로 열린 파일의
옵션을 확인(O_NONBLOCK / O_NDELAY등..) 그 후 H/W를 다루는 함수 inb(), readb()등을 호출하여 buf에 값을
불러오고 커널 함수 호출로 buf의 데이터를 전달합니다. 그 후 반환값으로는 처리된 데이터 개수(byte)를
반환합니다.
write
write 함수도 read함수와 비슷합니다. 단지 값을 읽어 오는게 아닌 buf에 있는 값을 H/W로 보냅니다.
1 | ssize_t ???_write(struct file *filp, char *buf, size_t count, loff *f_pos) |
반환값은 내보낸 데이터의 크기(byte)입니다.
받는 인자들은 read와 같습니다. buf에 보낼 데이터가 있는지 검사후 블록 옵션을 처리해주고
실제 outb(),writeb()등의 함수로 buf의 값을 장치로 보냅니다.
close
디바이스 드라이버에서 close는 ???_release()라고 쓰입니다.(커널에 적재는 유지 그래서 release가 아닌가..)
응용이 장치 이용 후 종료할 때 호출합니다.
1 | int ???_release(struct inode* inode, struct filp* filp){ |
release는 open과 같은 인자를 받고 정상 수행되면 0을 반환하고
문제가 있으면 음수를 반환하게 됩니다. 응용에서는 호출해주고 다른 처리는 하지 않습니다.
부록
filp 구조체의 변수(필드)중 디바이스 드라이버가 참조하는 것은 f_flags, f_pos, *f_op가 있습니다.
f_flags : 응용이 open시 줬던 옵션 정보가 있습니다. O_RDWR, O_NONBLOKC, O_NDELAY…
f_pos : 프로그램이 적재되어 있는 주 메모리에서의 위치. lseek 함수로 읽기/쓰기 위치를 조정합니다.
*f_op : file operation 구조체의 주소를 저장 하고 있는 포인터 입니다. 이 값을 이용하여 fop를 다르게 설정 가능.
커널 함수(Read)
copy_to_user(받을 곳, 보내는 곳,크기) : 보내는 곳(커널)에서 받을 곳(사용자)으로 n 바이트 만큼 복사합니다.
put_user(변수,주소) : 변수의 값(커널)을 주소(사용자) 메모리에 저장합니다.
커널 함수(write)
copy_from_user(받을 곳,보내는 곳, 크기) : 보내는 곳(사용자)에서 받을 곳(커널)로 n바이트 만큼 복사합니다.
get_user(변수,주소) : 변수(커널)에 주소(사용자)의 메모리값을 저장합니다.
read/write에서 실제 H/W에 값이 읽고 쓰기 되는 것은 in(), out(), read(), write()등의 함수로 작동됩니다.
함수 뒤에 b,w,l이 붙을 수 있는데 이는 처리 단위를 나타냅니다. b : byte, w : word, l : long
어셈블러도 비슷하게 접미사가 붙습니다. 어떤 함수들을 호출 하냐는 시스템의 i/o 처리 방법에 달려 있습니다.
I/O mapped I/O가 있고 Memory mapped I/O가 있는데 상세 내용은 컴퓨터 구조 과목에서 배웁니다.
간략하게 설명 하자면 시스템 버스를 데이터와 I/O가 같이 사용하면 후자이고 분리되 있으면 전자입니다.
intel 계열의 cpu는 I/O mapped를 사용하고 ARM 계열 cpu는 Mememory mapped를 사용합니다.
그리고 open시 블록 옵션을 주는 이유는 커널 영역에서 슬립이 일어나면 다른 중요한 일들이 처리가 안되기
때문에 O_NONBLOCK이나 O_NDELAY 옵션을 주어 읽을 수 있을 때까지 읽고 바로 종료시켜 슬립 상태로 가는걸
막기 위해서 입니다.
시간이 된다면 여기에는 적지 않았던 lseek()와 ioctl()에 대해서도 적어 보겠습니다.