디바이스 드라이버3 - file_operations

kenGwon·2024년 2월 22일
0

[Embedded Linux] BSP

목록 보기
26/36

이제부터 진짜 본격적으로 디바이스 드라이버를 만들어보겠다. 디바이스 드라이버의 핵심파트 시작..!

핵심 목적: 우리가 만든 디바이스 드라이버 파일과 어플리케이션 파일이 서로 상호작용하는 것(교재 페이지 162~ 193 정독 필요)

이제 진짜 디바이스 드라이버를 만들어보겠다.


모듈 사용횟수 관리

모듈 사용 횟수는 어플리케이션이 디바이스드라이버 모듈을 사용하고 있는지 아닌지를 판단할 수 있는 정보이다.

call_open()할 때 try_module_get(THIS_MODULE);을 추가하고,
call_release()할 때 module_put(THIS_MODULE);을 추가하면 된다.


소스파일

Makefile

MOD := call_dev
SRC := $(APP).c
obj-m := $(MOD).o

CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
CC := arm-linux-gnueabihf-gcc
KDIR := /home/ubuntu/pi_bsp/kernel/linux
PWD := $(shell pwd)

default:$(APP)
    $(MAKE) -C $(KDIR) M=$(PWD) modules $(CROSS)
    cp $(MOD).ko /srv/nfs
$(APP):
    $(CC) $(APP).c -o $(APP)
    cp $(APP) /srv/nfs
clean:
    rm -rf *.ko
    rm -rf *.mod.*
    rm -rf .*.cmd
    rm -rf *.o
    rm -rf modules.order
    rm -rf Module.symvers
    rm -rf $(MOD).mod
    rm -rf .tmp_versions
    rm -rf $(APP)

call_dev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>

#define CALL_DEV_NAME   "calldev" // 여기에서 이 이름은 별명 같은거다. 반드시 디바이스파일의 이름과 같을 필요는 없다.
#define CALL_DEV_MAJOR  230  // 디바이스 주번호

static int call_open(struct inode *inode, struct file *filp)
{
    int num = MINOR(inode->i_rdev);
    printk("call open-> minor : %d\n", num);

    num = MAJOR(inode->i_rdev);
    printk("call open-> major : %d\n", num);
    
    try_module_get(THIS_MODULE);
    return 0;
}

static loff_t call_llseek(struct file *filp, loff_t off, int whence)
{
    printk("call llseek -> off : %08X, whence : %08X\n", (unsigned int)off, whence);
    return 0x23;
}

static ssize_t call_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
    printk("call read -> buf : %08X, count : %08X \n", (unsigned int)buf, count);
    return 0x33;
}

static ssize_t call_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
    printk("call write -> buf : %08X, count : %08X \n", (unsigned int)buf, count);
    return 0x43;
}

static long call_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    printk("call ioctl -> cmd : %08X, arg : %08X\n", cmd, (unsigned int)arg);
    return 0x53;
}

static int call_release(struct inode *inode, struct file *filp)
{
    printk("call release \n");
    
    module_put(THIS_MODULE);
    return 0;
}

// 구조체 static 전역 변수
struct file_operations call_fops = {
    //.owner          = THIS_MODULE,
    .llseek         = call_llseek,
    .read           = call_read,
    .write          = call_write,
    .unlocked_ioctl = call_ioctl,
    .open           = call_open,
    .release        = call_release, // 저수준 입출력함수에서 close()가 .release에 대응한다.

};

static int call_init(void)
{
    int result;
    printk("call call_init \n");
    result = register_chrdev(CALL_DEV_MAJOR, CALL_DEV_NAME, &call_fops);
    if (result < 0)
        return result;
    return 0;
}

static void call_exit(void)
{
    printk("call call_exit \n");
    unregister_chrdev(CALL_DEV_MAJOR, CALL_DEV_NAME);
}

module_init(call_init);
module_exit(call_exit);
MODULE_LICENSE("Dual BSD/GPL");

# 첫번째 핵심


이 코드 부분이 핵심이다.
좌측은 어플리케이션에서 호출할 함수의 형태를 의미하고, 우측은 디바이스 드라이버 모듈 소스코드 안에서 선언한 함수 포인터를 의미한다.
어플리케이션에서 좌측 함수가 호출되면, 연결되어있는 우측의 디바이스 드라이버 모듈 함수 포인터를 찾아가서 실행하는 구조이다.

저 좌측에 있는 함수 포인터들은 전부 file_operations라는 구조체에 이미 정의되어 있는 함수 포인터이다. 이미 구조체가 헤더파일에 정의가 되어있기 때문에 이런식으로 선언과 동시에 초기화가 가능한 것이다.

# 두번째 핵심

result = register_chrdev(CALL_DEV_MAJOR, CALL_DEV_NAME, &call_fops); 구조체 주소를 등록하는 것이다.

중요한 것은 이 모듈을 rmmod할 때 반드시 unregister_chrdev() 함수를 호출해줘야 한다는 것이다. 그래야 다음번에 다시 모듈을 등록할 때 캐릭터 디바이스가 등록이 된다.(GPIO 자원을 설정하고 free하는 것과 같은 맥락)

# 세번째 핵심

함수 선언부를 보다 보면 파라미터에 loff_t *f_pos가 들어가 있는 함수들이 있다. 이 파라미터는 커널에서 자체적으로 관리해주는 파라미터이기 때문에, 어플리케이션에서 해당 함수를 호출할 때 해당 파라미터에 대한 아규먼트는 넣어주지 않아도 된다.

예를 들어, call_dev.c에서
static ssize_t call_read(struct file *filp, char *buf, size_t count, loff_t *f_pos) 라고 정의되어 있는 함수를,

call_app.c에서 호출할 때는
ret = read(dev, (char *)0x30, 0x31);라고 해서 arguments를 struct file *filp, char *buf, size_t count 세 개만 넣어준 것을 확인할 수 있다.

# 네번째 핵심

주번호 부번호는 32비트 머신의 size_t에서, 상위 12비트는 주번호로 하위 20비트는 부번호로 지정되어있다.
num = MINOR(inode->i_rdev);
num = MAJOR(inode->i_rdev);
주번호와 부번호를 가져오는 매크로 함수 내부를 까보면 이렇게 되어있다.
아주 기가막히게 매크로 함수가 구현 되어있다.

#define MINORBITS   20
#define MINORMASK   ((1U << MINORBITS) - 1)

#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))

call_app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>

#define DEVICE_FILENAME "/dev/calldev"

int main(void)
{
    int dev;
    char buff[128];
    int ret;

    printf("(!) device file open\n");

    dev = open(DEVICE_FILENAME, O_RDWR | O_NDELAY);
    if(dev >= 0)
    {
        printf("(2) seek function call dev: %d\n", dev);
        ret = lseek(dev, 0x20, SEEK_SET);
        printf("ret = %08X\n", ret);

        printf("(3) read function call\n");
        ret = read(dev, (char *)0x30, 0x31);
        printf("ret = %08X\n", ret);

        printf("(4) write function call\n");
        ret = write(dev, (char *)0x40, 0x41);
        printf("ret = %08X\n", ret);

        printf("(5) ioctl function call\n");
        ret = ioctl(dev, 0x51, 0x52);
        printf("ret = %08X\n", ret);

        printf("(6) device file close\n");
        ret = close(dev);
        printf("ret = %08X\n", ret);
    }
    else
        perror("open");

    return 0;
}

# 첫번째 핵심

dev = open(DEVICE_FILENAME, O_RDWR | O_NDELAY); 디바이스 파일을 열 때 블로킹을 하지 않겠다는 모드로 열겠다는 의미다(O_NDELAY = O_NONBLOCK). 읽을 데이터가 없으면 바로 0을 리턴하면서 프로세스를 잠재우지 않겠다는(블로킹하지 않겠다는)것이다.

리눅스에서 open()은 기본적으로 열때 읽을 데이터가 없으면 블로킹 모드로 진입하여 프로세스가 sleep상태에 들어간다. 게다가 C는 절차지향형 언어이기 때문에 open()에서 블로킹이 되어버리면 그 아래 코드가 하나도 실행되지 않게된다.

이러한 문제를 해결하기 위해서 등장한 개념이 입출력 다중화이다. 아주아주 중요한 개념이고, 이전에 분석했던 채팅방 코드에도 바로 이러한 입출력 다중화 개념이 사용되었다.



실행방법

교재 189페이지를 약간 변형해서 따라하면 된다.

// 디바이스 드라이버 활성화
pi@pi14:/mnt/ubuntu_nfs $ sudo mknod /dev/calldev c 230 32
pi@pi14:/mnt/ubuntu_nfs $ sudo insmod call_dev.ko

// 어플리케이션 실행
pi@pi14:/mnt/ubuntu_nfs $ sudo ./call_app
(!) device file open
(2) seek function call dev: 3
ret = 00000023
(3) read function call
ret = 00000033
(4) write function call
ret = 00000043
(5) ioctl function call
ret = 00000053
(6) device file close
ret = 00000000

// 디바이스 드라이버 비활성화 후 어플리케이션 실행
pi@pi14:/mnt/ubuntu_nfs $ sudo rmmod call_dev
pi@pi14:/mnt/ubuntu_nfs $ sudo ./call_app
(!) device file open
open: No such device or address

/dev/calldev : 어플리케이션에서 어떤 디바이스 파일을 열었는지
c : 캐릭터 디바이스라는 뜻
230 : 주번호가 230이라는 뜻
32 : 부번호가 32라는 뜻

해설

// 아무것도 안한 상태에서 어플리케이션 실행
pi@pi14:/mnt/ubuntu_nfs $ ./call_app
(!) device file open
open: No such file or directory

No such file or directory에서 주목해야 할 것은 file or directory이다. 디바이스 드라이버 파일을 만들지 않아서 그것을 찾지 못했다는 뜻이다.
그래서 디바이스 파일을 만들어주겠다.

pi@pi14:/mnt/ubuntu_nfs $ sudo mknod /dev/calldev c 230 32

여기서 중요한 것은 이 디바이스 파일을 만들 때 주번호와 부번호를 반드시 모듈 파일(call_dev.c)에서 정의한 주번호랑 같게 해서 만들어야 한다는 것이다.

pi@pi14:/mnt/ubuntu_nfs $ sudo ./call_app
(!) device file open
open: No such device or address

No such device or address에서 주목해야 할 것은 address이다. 디바이스 파일은 찾았는데, 모듈이 메모리에 적재되지 않았다는 뜻이다.
그래서 모듈을 적재해주겠다.

pi@pi14:/mnt/ubuntu_nfs $ sudo insmod call_dev.ko
pi@pi14:/mnt/ubuntu_nfs $ sudo ./call_app
(!) device file open
(2) seek function call dev: 3
ret = 00000023
(3) read function call
ret = 00000033
(4) write function call
ret = 00000043
(5) ioctl function call
ret = 00000053
(6) device file close
ret = 00000000

그럼 이제 제대로 실행되는 것을 확인할 수 있다.


이 호출구조가 가능한 이유

교재 162쪽부터 192쪽까지 제대로 리딩하면서 호출 플로우를 따라가봐야 한다.

가장 중요한 부분은 file_operations 구조체 필드저수준 입출력 함수를 통해서 inode에 접근하면서 상호작용이 일어나는 부분이다.

앞으로

교재 4개정도의 챕터를 활용하여 하나의 프로그램을 작성하는 시험이 나올 것이다.
교재 10장부터 14장까지의 내용을 가지고 3시간동안 프로그램을 작성하는 시험을 볼 것이다.(교재 10장부터 14장까지가 핵심이다.) 어플리케이션 코드는 완성된 코드를 주시는거고, 우리는 디바이스 드라이버 파일만 만드는 식으로 시험이 진행된다.

(read()의 인수인 buf가 void* 자료형으로 되어있기 때문에, 구조체도 버퍼에 담아 올수 있다는 점을 체크해두고 있자.)

profile
스펀지맨

0개의 댓글