디바이스 드라이버라는 소프트웨어의 일반 구조에 대해 알아봅시다.

입출력 장치는 크게 3가지로 분류하였습니다. 입출력 정보 단위가 한 바이트인 문자형 장치, 한 블록씩 장치와 주고받는 블록형 장치, 네트워크 장치가 있습니다. 이 세 장치가 특성이 다른 만큼 디바이스 드라이버 기능도 다르지만, 공통적인 소프트웨어 구조를 가집니다. 이번 시간에는 리눅스 디바이스 드라이버를 예로 해서 디바이스 드라이버를 설명하겠습니다.
리눅스 운영체제의 디바이스 드라이버는 매우 간결한 구조를 가집니다.
먼저, 초기화 함수 init() 함수, 파일시스템을 위한 인터페이스 함수, 하드웨어를 위한 인터페이스 함수로 구성됩니다.
운영체제의 파일 관리를 배울 때, 파일 연산을 설명한 적이 있는데, 파일 시스템 인터페이스가 그 파일 연산 함수입니다. 장치를 초기화하는 함수는 init()함수가 담당하는데, 디바이스 드라이버를 초기화하고 커널에 등록하는 역할을 합니다. 파일 시스템과 인터페이스 하는 함수에는 open, release, read, write등의 함수가 있는데, 파일 operations라고 하는 자료구조에 시작주소가 기록됩니다. 하드웨어와 인터페이스는 블록형 장치인 경우, in(), out()함수가 있습니다.

리눅스 디바이스 드라이버는 커널과의 통신을 위해 switch table을 이용합니다.
드라이버를 커널에 등록시킬 때 사용하는 함수가 register함수인데, 주장치번호, 디바이스의 이름, 파일 연산의 자료구조의 시작 주소를 인자로 주게 됩니다. 만약에 터미널 드라이버의 경우, 번호가 4가 되며, 이름은 "tty", 파일 연산은 &tty_fops가 됩니다. 만약에 line printer인 경우, 그림과 같습니다. register_chrdev에 인자를 줘서 등록합니다.
초기화하는데서 register를 하고, 장치와 커널이 통신하는 방법이 switch table을 이용한다고 했는데, 문자형 장치를 위해서는 chrdevs[] table을 사용하고, 블록형 장치인 경우, blkdevs[], blk_dev[] tables를 사용합니다.


대표적인 문자 디바이스 드라이버인 tty, 즉, 터미널인 키보드입니다. 키보드의 디바이스 드라이버를 알아봅시다. 디바이스 드라이버는 초기화 함수, 파일시스템과 인터페이스하는 함수, 장치와 인터페이스하는 함수로 구성되어있습니다. 일반적으로, 디바이스 드라이버 구성 함수는 디바이스를 쉽게 구별하기 위해 키보드인 tty_를 앞에 붙여서 tty_open(), tty_read()와 같이 나타냅니다. 문자 디바이스 드라이버 구조를 살펴봅시다. 왼쪽의 open(), read(), write()등은 파일 시스템과 인터페이스 하기 위한 함수이며, 이 함수들은 리눅스 소스들에서 driver/char/tty_io.c에 정의가 되어있습니다. tty_open이라는 함수는 디바이스를 open할 때 호출하는 함수이며, release는 디바이스를 사용하지 않을 떄 하는 함수이며, tty_read()는 키보드 입력을 읽을 때 하는 함수이며, tty_write()는 출력, tty_ioctl는 입출력 제어를 위한 함수입니다. 그래서 새로운 문자 디바이스 드라이버를 구현할 떄, 최소한, read(), write(), ioctl(), open(), release()는 반드시 구현해야 합니다. 그리고 이 함수들의 시작 주소들이 file operations의 시작 주소가 등록됩니다. 다음의 터미널 하드웨어와 인터페이스하는 함수로 rs_interrupt()와 receive_chars(), ransmit_chars()가 있습니다. rs_interrupt()함수는 iddt테이블에 등록되며, 터미널의 인터럽트 발생 시 호출되는 인터럽트 서비스 루틴입니다. 인터럽트 핸들러가 인터럽트 디스크립터 테이블을 조사해서 번호에 따른 인터럽트 서비스 루틴을 부른다고 했습니다. ransmit_chars()는 character의 터미널의 문자를 출력하는 함수이고, receive_chars()는 터미널의 문자를 읽는 함수입니다. 결국, tty_read()를 하는 경우, 터미널에서 글자를 읽으므로 receive_chars()라는 함수를 내부적으로 호출합니다. tty_write()의 경우는 실제 하드웨어에 보내는 역할을 하는 ransmit_chars()함수를 호출합니다. ty_init()는 초기화하는 함수인데, 터미널 디바이스 드라이버를 초기화시키고, 디바이스 드라이버를 등록하는 역할을 합니다. 디바이스 드라이버도 모듈 형태로 구현을 하는데, 커널이 초기화되면서 모듈(동적으로 리눅스 운영체제에 플러그인 되었다가 또 없어질 수 있는 소프트웨어 단)이 커널에 적재될 때나 커널에 부팅해서 커널을 처음 초기화할때 호출이 되어서 실행됩니다. 터미널 디바이스 드라이버를 초기화시킬 때 하는 일은 tty struct라는 자료구조를 초기화하는 것이고, rs_init()라는 함수를 호출해서 serial line(?)을 초기화시킵니다.


tty-init()가 하는 일을 다시 한 번 살펴봅시다.
init process가 커널이 부팅과정에서 초기화될 때 실행되는데, tty_init()를 부를 수 있습니다. 또는 모듈 형태로 디바이스 드라이버가 추가된다고 하면, 거기에 관련된 함수인 init_module이 tty_init()를 부를 수 있습니다.
그래서 tty_init()이 호출되면, register_chrdev(TTY_MAJOR, "tty", &tty_fops);를 통해 TTY_MAJOR가 4이므로 등록을 하면, tty라는 장치 이름이 있고, tty_fops라는 파일 연산의 시작 주소가 쓰여져서 등록을 합니다.
register_chrdev()함수는 캐릭터 디바이스의 스위치 함수를 찾습니다. 캐릭터 디바이스 스위치 함수의 메이저 넘버에 해당되는 칸에 집어넣는데, 캐릭터 디바이스 스위치 테이블이 어떻게 생겼는지 먼저 살펴봅시다.
캐릭터 디바이스 스위치 테이블은 struct device_strut가 쭉 연결된 형태입니다. 그 struct에는 name과 fops가 들어갑니다. 각각이 Major number로 식별됩니다. 만약 터미널이 4번일 경우, 4번 칸에 name은 tty가 들어갈 것이고, fops는 레지스터 캐릭터 디바이스에서 tty fop의 시작주소를 줬는데, 레지스터 함수가 바로 캐릭터 디바이스 스위치 테이블의 4번째 칸에 들어가게 됩니다. 그래서 그때, chrdevs[major].name = "tty" (device name)과 chrdevs[major].fops = &tty_fops (file operations)를 실행합니다. 그래서 그것이 테이블에 저장됩니다.
이런 과정이 tty-init()함수입니다.




자, 등록된 터미널 드라이버는 이 디바이스를 나타내는 장치 파일을 이용해서 접근할 수 있습니다.
사용자가 응용 프로그램에서 open이라는 시스템콜을 부르면, sys_open()이 호출되게 됩니다. sys_open()함수는 filp_open()이라는 함수를 호출하여 open_namei()라는 함수를 호출합니다. 루트에서 시작하여 계층적으로 밑으로 내려가면서 open해야 할 장치이름, 오픈할 파일이 /dev/tty0라고 하면, 루트 밑에 dev가 있는지 조사하고, 그 및의 tty0이라는 장치파일이 있는지 조사하고 있으면 file control block의 식별자인 inode number를 return해줍니다. 그러면 inode number로 inode를 읽으면 tty0라는 파일 속성을 읽으니까 tty0가 장치 파일이면서 캐릭터 타입 디바이스 파일이라는 것을 FCB를 통해 알 수 있게 됩니다. 그러면 문자형 장치라는 것을 알고, major number와 minor number를 알아야 하는데 inode에서 읽어낼 수 있습니다. tty0이면, 주번호는 4번이고, 부번호는 0번이라는 것을 FCB에 저장되어있으니까 알게됩니다. 그러면 struct파일이라는 자료구조를 만들고, 정보값을 채우는데, struct file_operations * f_op라는 변수에 문자 디바이스를 넣으면 파일 연산을 저장하여, char.device file의 시작 주소를 집어넣습니다.그래서 struct file을 만들고 초기화 시키고, 문자형 디바이스의 f_op open을 기록합니다. 주의할 것은 지금 현재는 chrdev's file operations를 가리킨 다는 것을 기억합시다!
char.device file은 fs/device.c에 정의되어있는데, chrdev_open은 3가지 작업을 수행합니다. 첫번쨰로는 major number를 인덱스로 하여 char device switch table의 항을 찾습니다. chrdevs[]에서 major number 4를 찾아가서 fops의 값을 읽어서 파일이라는 data structure에 복사해서 넣습니다. 결국 struct file은 chrdev's file operations의 시작 주소를 가리키는 것이 아니라 tty's file operations의 시작 주소를 가리킵니다.
그러니까 flip_open()에서 chrdev_open()을 호출하면, character device switch table의 장치 번호에 해당되는 fop를 덮어쓰는 것이어서 이제는 tty open을 가리키게 됩니다.
이렇게 하는 이유는 문자형 장치에도 여러가지 타입이 있기 때문에 먼저 일반적인 문자형 타입의 open만 불러서 그다음에 character device swtich table을 거쳐서 실제 장치(마우스, 키보드 등) 의 파일 연산을 교체해주는 작업을 open에서 하는 것입니다. 이렇게 교체가 되면, 처음에는 일반적인 문자형 장치의 fopen을 거쳐서 chrdev_open이라는 함수가 실제 장치에 해당되는 open으로 바뀌어졌기 때문에 이제는 tty_open으로 바꿀 수 있는 것입니다.
이렇게 대체를 한다는 것을 잊지 말아야 합니다!

다음의 tty_read()인 경우를 보면, 앞에서 open과정을 통해서 tty를 위한 파일 연산으로 바뀌었고, 그러면 이제 tty_read로 바뀌는 것입니다. sys_read()가 struct file의 f_p[의 read함수를 부릅니다. f_op가 바뀐 것에 대해서 read를 해서 tty_read를 호출합니다. 그래서 프로그래머는 터미널이 제공하는 tty_read()로 터미널에서 읽습니다. 일반적인 캐릭터 디바이스 오픈 함수 내에서 메이저 넘버를 바꿔써서 tty_read()를 부르면, receive_char()인 하드웨어와 인터페이스하는 값을 읽게 됩니다.
'운영체제및실습' 카테고리의 다른 글
| [운영체제및실습] 11주차 3회차: 메모리관리 개요 (0) | 2025.05.25 |
|---|---|
| [운영체제및실습] 11주차 2회차: RAID (0) | 2025.05.25 |
| [운영체제및실습] 10주차 4회차: 디스크 스케줄링 (0) | 2025.05.25 |
| [운영체제및실습] 10주차 3회차 : I/O제어 : 인터럽트 I/O, DMA (0) | 2025.05.12 |
| [운영체제및실습] 10주차 2회차: I/O제어 : 폴링 (0) | 2025.05.10 |