ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Linux Kernel] Device File : Read, write (with ioctl)
    프로그래밍/리눅스 시스템 2024. 4. 29. 03:36
    Intro

     

    https://ballbigdiary.tistory.com/11

     

    Device Driver 적재, Device File 생성

    커널모듈 형태(.ko)의 Device Driver 제작 module_init(함수); // 커널모듈 적재시 호출되는 함수 지정 매크로module_init (함수)기능 : 현재 커널 모듈을 등록(insmod)할 때 호출되는 함수를 지정하는 매

    ballbigdiary.tistory.com

     

    ▲ 커널모듈 형태의 device driver를 kernel에 적재하고,

    device dirver와 연결되는 device file을 생성하는 방법에 대해 다루었다.

     

    이번에는 app에서 device file에 syscall 요청을 통해

    device 장치를 read, write하는 방법에 대해 알아보자!!

     

     

     


     

    Make file

    Makefile : user app과 device driver를 모두 build 해주어야 한다.
    #app.c도 같이 build 하기 위해 수정
    KERNEL_HEADERS=/lib/modules/$(shell uname -r)/build
    CC = gcc
    
    TARGET := app
    obj-m += devicedriver.o
    
    PWD := $(CURDIR)
    
    #make 시 make all 실행
    all: driver app
    
    #driver build
    driver:
        make -C $(KERNEL_HEADERS) M=$(PWD) modules
    
    #app build
    app:
        $(CC) -o $@ $@.c
    
    #driver, app 모두 제거
    clean:
        make -C $(KERNEL_HEADERS) M=$(PWD) clean
        rm -f *.o $(TARGET)

     

    • all : make 명령어입력시 기본으로 실행되는 명령
      • all의 target은 driver, app이기 때문에, driver와 app이 실행된다.
    • driver : 이전과 동일. 커널 헤더에 obj-m을 커널모듈로 build한다.
    • app : gcc명령어를 이용해 app.c파일을 app.o로 build한다.

     

     

    App

     

     

    권한 설정

    $ sudo chmod 666 [/dev/deviceFile명]

    • App에서 device file에 접근할 수 있도록 권한을 수정해주어야 한다.

     

     

    open() : 파일 오픈 => .open 호출한다.

    int open(const char* path, int flag)

    • 기능 : 파일 시스템에서 path을 열어서 해당 파일에 대한 파일 디스크립터를 생성
    • 입력값 
      • path : 열고자 하는 파일의 경로
      • flag : 파일을 어떻게 열지를 지정하는 플래그, 필수옵션과 추가 옵션이 존재하며 두 옵션은 OR(|)기호로 연결한다.
      • [flag 필수 옵션]
        > O_RDONLY : 파일을 읽기 전용으로 연다.
        > O_WRONLY : 파일을 쓰기 전용으로 연다.
        > O_RDWR : 파일을 읽기 및 쓰기용으로 연다.
        [flag 추가옵션]
        > O_CREAT : 파일이 존재하지 않는 경우 새로운 파일을 새로 생성
        > O_APPEND : 덧붙이기 (쓰기용과 달리, 기존 내용은 유지한 채, 파일의 끝에 추가하여 쓰기 작업을 수행)
        > O_TRUNC : 파일 내용 제거 후 사용 (기존 파일의 내용을 모두 삭제하고 해당 파일 사용)
        => O_APPEND / O_TRUNC 없이 그냥 write()하는 경우, 기존 내용에 덮어쓰기 된다.
      • => flag 필수 옵션과 추가 옵션은 | 기호로 연결한다. ex) O_RDWR | O_CREAT
    • 반환값
      > 성공시 : open한 파일의 파일 디스크립트(int 양의 정수)를 반환
      > 실패시 : -1을 반환
    • 필요 라이브러리
      • #include <sys/types.h>
        #include <sys/stat.h>
        #include <fcntl.h>

     

     

     

    close() : 파일 닫기 => .release 호출한다.

     

    int close(int fd)

    • 기능
      • 파일 디스크립터를 닫는 시스템 콜.
      • 파일 디스크립터를 닫으면 해당 파일에 대한 모든 작업이 완료되며, 시스템 리소스가 해제된다.
      • 파일을 열고 사용한 후에 반드시 호출되어야 한다.
    • 입력값 fd : 닫고자 하는 파일의 디스크립트
    • 반환값 
      > 성공시 :  0을 반환
      > 실패시 : -1을 반환
    • 필요 라이브러리 : #include <unistd.h>

     

     

    read() : 파일 읽기

     

    ssize_t read(int fd, void buf[.count], size_t count)

    • 기능 : fd에 해당하는 파일을 파일 오프셋 위치에서 부터 count 크기만큼 읽어서 buf에 저장한다.
    • 입력값
      • fd : 읽고자 하는 파일의 fd (open() 함수의 반환값)
      • buf : 파일에서 읽은 내용을 저장할 char buffer
      • count : 파일에서 읽고자하는 사이즈 (byte단위)
    • 반환값
      > 동작이 성공 : 읽은 바이트 수를 반환
      > 오류 시 : -1 반환
    • 필요 라이브러리 : #include <unistd.h>

      💡"unistd" = "Unix Standard"
      Unix 시스템에서 표준 입출력 함수나 파일 조작 함수 등을 정의하는 헤더 파일 중 하나
      Unix 시스템에서 프로그램을 작성할 때, 파일 입출력이나 시스템 콜(System Call) 등을 다룰 때 주로 사용된다.

     

     

     

    wite() : 파일 쓰기

     

    ssize_t write(int fd, const void buf[.count], size_t count )

    • 기능 : buf에 저장된 내용을 count만큼 fd의 파일 오프셋 위치에서부터 작성한다.
    • 입력값
      • fd : 쓰고자 하는 파일의 fd (open() 함수의 반환값)
      • buf : 파일에 쓸 내용이 저장된 buffet의 주소
      • count : 파일에 쓸 사이즈 (byte단위)
    • 반환값
      > 성공시 0 
      > 오류 시 -1
    • 필요 라이브러리 : #include <unistd.h>

     

     

    ⭐ ioctl() : 하드웨어를 제어하기 위한 syscall => 주로 이것을 사용한다!

    int ioctl( int fd, unsigned long request,void * arg )

        //ioctl로 /dev/deviceFile 에 _IO() 매크로로 arg 값 전달
        ioctl(fd, _IO(0,3), 16);        //16 decimal
        ioctl(fd, _IO(0,4), 0xf);       //15 hex
        ioctl(fd, _IO(0,5), 0b1111);    //15 binary
        int ret = ioctl(fd, _IO(0,6), 0);
    
        //ioctl 의 return 값으로 error 검출
        if(ret < 0){
            printf("%d command invalid!\n", ret);
        }
    • 기능
      • 장치나 파일에 대한 다양한 제어 작업을 수행 ( 장치 설정 변경,정보 요청, 사용자 정의 동작 실행 등..)
    • 입력값
      • fd : 제어할 device file의 descriptor
      • request : 요청 코드 또는 명령을 나타내는 값. cmd parameter 규칙에 맞춰 작성한다.
      • arg : ioctl() 요청시 전달할 인자. request에 따라 적절한 인자전달 + 캐스팅 되어야 한다.
        (여러개를 보낼 때는 구조체 포인터를 전달)
    • 반환값 : 성공시 0을 반환, 실패할 경우 -1을 반환
    • #include <sys/ioctl.h> 필요
    • ex) ioctl(fd, _IO(0,3), 0); 
      • _IO 매크로는 일반적으로 간단한 제어 명령을 지정하는 데 사용되는 것으로, 여기서는 사용자 정의 명령을 나타낸다.
      • arg값으로 0을 전달하는 경우, 이는 해당 명령에 필요한 추가 인수가 없음을 나타낸다.
      • _IO() 사용시, _IO(0,1) / _IO(0,2)는 시스템에서 사용하고 있기 때문에, _IO(0,3) 부터 사용가능하다.

     

     

     

    ▶ request cmp parameter 작성을 돕는 매크로

    • _IO(type, number) : 단순한 타입
    • _IOR(type, number, 전송 받을 데이터 타입) : read용
    • _IOW(type, number, 전송 보낼 데이터 타입) : write용
    • _IOWR(type, number, 전송 주고 받을 데이터 타입) : read & write 둘다

     

    • type : 매크로 그룹 번호. 요청 코드를 그룹화하는 데 사용됩니다. 이는 주로 장치나 드라이버의 기능을 구분하는 데 사용된다.
    • number : 요청 번호. 해당 그룹(장치)내에서의 요청 코드를 지정. 명령을 식별하는데 사용된다.
    • type : 주고받을 데이터 타입 (int, char ...)

     

     

     

    device driver

     

    user space와 kernel space간 데이터를 전송하는 방법

     

    🚨User space 와 Kernel space 는 서로 메모리 접근이 되지 않으며,

    user space의 메모리 주소와 kernel space의 메모리 주소체계가 다르기 때문에, 

    app에 생성한 char*의 주소 ( 포인터 )를 kernel로 전송하면, 서로 다른 주소를 가리키게 된다.

    즉, app에서 단순히 read, write 함수를 사용하여 device 값을 읽을 수 있는게 아니다.

     

    => 따라서 device driver에서는 read/write 함수에대한 동작이 별도로 정의되어야 하며,

    device driver에는 다음의 함수를 이용해 user space와 데이터를 송/수신한다.

     

    • copy_to_user() : kernel space → user space 으로 메시지 전송
    • copy_from_user() : user space → kernel space 로 메시지 전송
      => copy함수는 포인터를 이용해 큰 메시지를 한번에 전송한다.
    • put_user() : byte 단위로 kernel space → user space로 메시지  전송할 때
    • get_user() : byte단위로  user space → kernel space로 메시지 전송할 때
      => byte 단위로 하나 하나 전송해서 더 안전하다.
    • ioctl() : 장치 및 파일 디스크립터와 상호 작용하여 I/O 장치를 제어하는 syscall이다.
      => ⭐이때, user app에서는 read/write를 사용하지 않고 ioctl함수로 device에게 요청한다!

     

     

    put_user(), get_user() 

     

    file_operations 구조체 정의

     

    ▶ fops에 read, write syscall에 대한 호출함수 추가

    static struct file_operations fops = {
        .owner = THIS_MODULE,
        .open = deviceFile_open,
        .release = deviceFile_release,
        //.write
    	.write = deviceFile_write,
    	//.read 
        .read = deviceFile_read,
    };
    • read syscall 호출시 →  deviceFile_read 함수 실행
    • write syscall 호출시 →  deviceFile_write 함수 실행

     

    ⭐이때, user에서 read()함수에 전달한 인자는 맵핑된 deviceFile_read함수에 전달된다! (write도 마찬가지)

     

     

     

     

    put_user() : user에게 1byte씩 데이터 보내기

     

    ▶  deviceFile_read() 함수 정의 with put_user()

    static ssize_t deviceFile_read(struct file *filp, char __user *buf, size_t cnt, loff_t *pos)

    • 기능 : put_user함수를 이용해 kernel space의 Device File의 메시지를 byte단위로 읽어 user space의 app에 전달
    • 입력값
      • filp : 접근 방식, 파일 포인터
      • buf : 읽은 바이트를 저장할 버퍼 (__user 매크로를 사용해서 버퍼가 user 영역의 메모리 공간임을 명시한다.)
      • cnt : 읽을 바이트 size 명시
      • pos : 파일의 오프셋으로, 읽기를 시작할 위치를 나타낸다. (NULL의 경우, 현재 오프셋이 그대로 유지된다.)
    • 반환값 : 읽은 바이트 수

     

     

    ▶ put_user() : byte 단위로  kernel space → user space로 메시지  전송

    long put_user(unsigned long val, unsigned long __user* to);

        // 요청된 길이 만큼 데이터를 읽고 사용자 공간으로 복사합니다.
        while ( cnt && *msg_ptr ) {
            put_user(*(msg_ptr++), buf++);  // 메시지 버퍼의 데이터를 사용자 버퍼로 복사합니다.
            cnt--;  // 읽은 데이터의 길이를 감소시킵니다.
            bytes_read++;  // 읽은 바이트 수를 증가시킵니다.
        }copy
    • 기능 : val위치의 데이터를 1byte읽어 to위치에 저장한다.
    • 입력값
      • val : 읽어올 데이터 위치 => kernel space에 있는 데이터 영역
      • to : 읽은 데이터를 저장할 위치 => user space에 데이터를 복사할 주소를 가리키는 포인터.
      • => put_user함수는 byte단위로 데이터를 전송하므로 모든 데이터를 다 읽을 때까지 "1 byte 넣가 → 주소 옮기기" 과정이 반복 되어야 한다.
      • => 따라서 두 값은 모두 후위 증감연산자가 사용된 것을 알 수있다. (순서 : 1byte읽기 => ++연산으로 이동하기)
    • 반환값 : 성공시 0, 실패시 오류
    • #include <arm/uaccess.h> 필요.

     

     

     

    get_uset() : user에게 1byte씩 데이터 받아오기

     

    ▶  deviceFile_write() 함수 정의 with get_user()

    ssize_t deviceFile_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *pos)

    • 기능 : get_user함수를 이용해 user space의 메시지를 byte단위로 읽어 kernel space의 device에 전달
    • 입력값
      • filp : 접근 방식, 파일 포인터 (작성할 kernel space)
      • buf : 작성할 데이터가 저장된 버퍼 (__user 매크로를 사용해서 버퍼가 user 영역의 메모리 공간임을 명시)
      • cnt : 작성할 바이트 size 명시
      • pos : 파일의 오프셋으로, 작성 시작 위치를 나타낸다. (NULL의 경우, 현재 오프셋이 그대로 유지된다.)
    • 반환값 : 작성한 바이트 수

     

     

    ▶ get_user() : byte 단위로  user space → kernel space로 메시지  전송

    long get_user(unsigned long to, unsigned long __user* val);

    	int i;
        for(i = 0; i<cnt; i++){
    		//user space data 복사
    		if( get_user(kernel_buffer[i], buf+i) != 0 ){
    			return -EFAULT;
    		}
    	}
    • 기능 : from 위치의 데이터를 1byte읽어 to위치에 저장한다.
    • 입력값
      • to : 데이터를 저장할 위치 =>  app에서 작성한 데이터를 저장해둘 kernel space의 주소
      • from : 데이터를 가져올 위치 => 작성할 데이터를 가리키는 user space의 주소소
    • 반환값 : 성공시 0, 실패시 오류
    • #include <arm/uaccess.h> 필요.

     

     

    🚨put_user의 경우, 인자가 from , to 순서로 전달되지만,

    get_user의 경우, 인자가 to, from 순서로 전달된다!!!

    즉, 항상 kernel 메모리, user 메모리 순서로 인자가 전달된다.

     

     

     


     

     

    copy_to_user(), copy_from_user()

     

    🚨get_user, put_user는 인자가 kernel, user 순으로 전달되지만, 

    copy_함수는 인자가 to , from 순으로 전달된다.

     

    또한 to, from 모두 (void*)로 형변환을 해주어야 한다!

     

    copy_to_user() : user에게 데이터를 전송

    long copy_to_user(void __user *to, const void *from, unsigned long n)

    static ssize_t deviceFile_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
        pr_alert("command number : %d\n", cmd);
       	
        char buf[30] = "THIS IS KERNEL DATA!";
        int ret; 
        switch(cmd){
            case _IO(0,3):
                ret = copy_to_user((void*)arg, (void*)buf, sizeof(buf));
    	    pr_info("Trasfer Data!\n");
                break;
        }
        return 0;
    }
    • 기능 : from 포인터가 가리키는 kernel 메모리 영역에서 n 바이트를 to 포인터가 가리키는 user영역 메모리로 복사
      => 한 번에 전달하기 때문에 반복문 필요 X
    • 입력값 
      • to : 복사한 데이터를 저장할 목적지를 가리키는 포인터 => user space 영역
      • from : 복사할 데이터의 소스 파일 가리키는 포인터 => kernel 메모리 영역
      • n: 복사할 데이터의 크기(바이트 단위)
    • 반환값 : 성공시 0
    • #include <linux/uaccess.h> 필요

     

     

    copy_from_user() : user로부터 데이터 수신

     long copy_from_user(void  *to, const void __user  *from, unsigned long n)

    static ssize_t deviceFile_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
        pr_alert("command number : %d\n", cmd);	
        char buf[30];
        int ret; 
        switch(cmd){
            case _IO(0,3):
                ret = copy_from_user((void*)buf, (void*)arg, sizeof(buf));
    	    pr_info("app data : %s\n", buf);
                break;
        }
        return 0;
    }
    • 기능 : from 포인터가 가리키는 user 메모리 영역에서 n 바이트를 to 포인터가 가리키는 kenel 영역 메모리로 복사
    • 입력값 
      • to : 복사한 데이터를 저장할 목적지를 가리키는 포인터 => kernel space
      • from : 복사할 데이터의 소스 파일 가리키는 포인터 => user space  
      • n: 복사할 데이터의 크기(바이트 단위)
    • 반환값 : 성공시 0
    • #include <linux/uaccess.h> 필요

     


     

    앞에선, user space에서 read, write syscall을 호출하는 예제였다.

    하지만, 주로 ioctl syscall을 이용해 device에 접근한다.

     

    file_operations 구조체 정의 - unlocked_ioctl 멤버변수 추가
    static struct file_operations fops = {
        .owner = THIS_MODULE,
        .open = deviceFile_open,
        .release = deviceFile_release,
        //ioctl 사용 시 kernel에 lock이 걸리는 현상 방지 ( semaphore )
        .unlocked_ioctl = deviceFile_ioctl,
    };
    •  ioctl() syscall에 대응될 API(deviceFile_ioctl)를 fops에 등록해준다.

     

     

    App 에서 보낸 ioctl() syscall에 대응할 API

     

    static ssize_t deviceFile_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)

    static ssize_t deviceFile_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
        //입력된 cmd 값 출력
        pr_alert("command number : %d\n", cmd); //pr_alert() : 경고 수준의 커널 로그 출력
        
        //ioctl의 _IO 매크로의 값에 따라 switch문 동작
        switch(cmd){
            case _IO(0,3):
                pr_info("3 - %lu\n", arg);
                break;
            case _IO(0,4):
                pr_info("4 - %lu\n", arg);
                break;
            case _IO(0,5):
                pr_info("5 - %lu\n", arg);
                break;
            default :
                //3~5 이외의 값 입력시, -EINVAL ERROR를 app으로 전송
                return -EINVAL;
        }
        return 0;
    }
    • 기능
      • 입력된 명령(cmd) 값에 따라 다양한 동작을 수행한다.
      • 여기서는 cmd argument로 전송된 값을 출력하도록 함수를 정의하였다.
      • switch 문을 이용하여, cmd값에 따라 동작을 정의해주었다.
        => arg 값을 %lu(unsigned long)으로 출력한다.
        => 정의되지 않은cmd에 대해서는 -EINVAL error 를 return해서 app 에서 디버깅할 수 있도록 함.
    • 입력값 
      • fd : device file desceiptor
      • cmd : 수행할 ioctl 명령을 나타내는 매개변수
      • arg: cmd 명령과 관련된 추가적인 인수(argument)
    • 반환값 
      • 성공시 : 0
      • 실패시 : 해당 오류를 나타내는 음수 값
      • 유효하지 않은 명령에 대한 경우 -EINVAL을 반환

     

     

    구조체를 이용해 여러개의 데이터를 한번에 넘기기
    static ssize_t deviceFile_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
        pr_alert("command number : %d\n", cmd);
       	
        struct Node readData;
        struct Node writeData = {255, "Kernel Struct Data"};
        int ret;  
        switch(cmd){
            case _IO(0,3):
                ret = copy_to_user((void*)arg, &writeData, sizeof(struct Node));
    	    pr_info("Trasfer Data!\n");
                break;
    	case _IO(0,4):
    	    ret = copy_from_user(&readData, (void*)arg, sizeof(struct Node));
    	    pr_info("Read Struct Data : %c %s\n", readData.n, readData.buf);
    	    break;
        }
        return 0;
    }
    • 어려울 것 없이, user에게 전달받은 메모리 영역을 (void*)로 형변환 해준뒤에 인자로 전달해주면 구조체를 전달할 수 있다! (= 여러개의 데이터 한번에 전달 가능)
Designed by Tistory.
-->