본문으로 바로가기

Kernel (3) - ioctl()

Contents

  1. Linux Kernel Function - ioctl()

  2. make test program and debuging


Linux Kernel Function - ioctl()

#include <sys/ioctl.h>
int ioctl(int d,int requset, ...);

success : return 0;
error : return -1;
  • first parameter : 파일 디스크립터

  • second parameter : 디바이스에 전달한 명령

  • arg : 가변인자 매게변수로 개발자의 설정에 따라 인자의 갯수 등이 달라질 수 있음

ioctl()read()write()함수만으로 해결되지 않는 제어에 사용된다.


read함수와 write함수는 데이터의 읽고 쓰기 등의 기능은 가능하지만 하드웨어를 제어하거나 상태 정보를 얻을 수 없다.

read()write()함수와 같이 읽기 쓰기를 처리할 수 있으며, 하드웨어의 제어나 상태 정보를 얻기 위해 사용한다.

예를틀어, CD-ROM드라이버에 실제 장치에서 디스크를 꺼내도록 지시하는 등의 low level의 하드웨어를 제어하는 행위는 ioctl()함수만 할 수 있다.

d함수가 호출되고 난 후 헤더파일을 참조하며 매크로를 처리해주고, switch문으로 cmd parameter를 검사한다.

그리고 하드웨어에 명령을 전달하여 명령을 수행한다.


/usr/include/asm/ioctl.h에는 iotcl()에 사용에 관한 매크로가 정의되어있으며, 해당 매크로를 통하여 여러가지 커맨드 매크로를 정의할 수 있다.

MacroDescription
_IO(int type, int number)type과 number만 전달하는 단순한 iotcl()에서 사용되어짐
_IOR(int type, int number, data_type)디바이스 드라이버에서 데이터를 읽는 ioctl()에서 사용되어짐 ( R - READ)
_IOW(int type, int number, data_type)디바이스 드라이버에서 데이터를 쓰는 iotcl()에서 사용되어짐 ( W - WRITE)
_IORW(int type, int number, data_tpye)디바이스 드라이버에서 데이터를 쓰고 읽는 iotcl()에서 사용되어짐 ( RW - READ, WRITE )


아래는 위의 매크로를 사용하여 커맨드를 매크로를 구현한 예시이다.

/* chardev.h */

:
:
:

struct ioctl_info{
      unsigned long size;
      unsigned int buf[128];
};
 
#define IOCTL_MAGIC 'G'
#define SET_DATA _IOW(IOCTL_MAGIC, 2 , ioctl_info )
#define GET_DATA _IOR(IOCTL_MAGIC, 3 , ioctl_info )

static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
   printk("The chardev_ioctl() function has been called.");
 
   switch (cmd) {
       case SET_DATA:
           printk("SET_DATA\n");
           if (copy_from_user(&info, (void __user *)arg, sizeof(info))) {
               return -EFAULT;
          }
       printk("info.size : %ld, info.buf : %s",info.size, info.buf);
           break;
       case GET_DATA:
           printk("GET_DATA\n");
           if (copy_to_user((void __user *)arg, &info, sizeof(info))) {
               return -EFAULT;
          }
           break;
       default:
           printk(KERN_WARNING "unsupported command %d\n", cmd);
 
       return -EFAULT;
  }
   return 0;
}

위 코드는, SET_DATA매크로와 GET_DATA매크로를 정의하였다.

따라서 ioctl()함수가 호출되고 cmd parameter가 해당 매크로일경우 해당 매크로에 선언되어있는 명령을 그대로 따라한다.

아래는 해당 chardev.h헤더파일을 사용하여 커널 공간에서 사용자 공간에 데이터를 복사하고, 그 데이터를 출력하는 예제이다.


/* test.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include "chardev.h"
 
int main()
{
   int fd;
   struct ioctl_info set_info;
   struct ioctl_info get_info;

   set_info.size = 100;
   strncpy(set_info.buf,"lazenca.0x0",11);

   if ((fd = open("/dev/chardev0", O_RDWR)) < 0){
       printf("Cannot open /dev/chardev0. Try again later.\n");
  }
 
   if (ioctl(fd, SET_DATA, &set_info) < 0){
       printf("Error : SET_DATA.\n");
  }


   if (ioctl(fd, GET_DATA, &get_info) < 0){
       printf("Error : SET_DATA.\n");
  }
 
   printf("get_info.size : %ld, get_info.buf : %s\n", get_info.size, get_info.buf);
 
   if (close(fd) != 0){
       printf("Cannot close.\n");
  }
   return 0;
}

위 코드는 다음과 같이 동작한다.

  1. open()함수를 이용하여 "/dev/chardev0" 파일을 열어 fd값을 얻는다.

  2. ioctl()함수를 이용하여 사용자 공간에 저장된 데이터를 커널 공간에 데이터를 복사한다.

  3. ioctl()함수를 이용하여 커널 영역에 저장된 데이터를 사용자 영역으로 복사한다.

  4. printf()함수를 이용하여 사용자 공간에 복사된 데이터를 출력한다.

  5. close()함수를 이용하여 fd를 닫는다.

이제 커널을 로드하고, 테스트 프로그램을 돌려보자.


make test program and debuging

obj-m := chardev.o

all:
   make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
   make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

위와 같은 Makefile을 만들어주고, make 명령을 실행시킨다.


juntae@ubuntu:~/kernel/ioctl$ make
make -C /lib/modules/4.15.0-65-generic/build M=/home/juntae/kernel/ioctl modules
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-65-generic'
Building modules, stage 2.
MODPOST 1 modules
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-65-generic'

make명령으로 빌드를 완료했으므로 insmod명령으로 커널 모듈을 로드시킨다.


juntae@ubuntu:~/kernel/ioctl$ sudo insmod chardev.ko

그리고 lsmod명령과 grep명령으로 해당 모듈의 로드 여부를 확인해보면 아래와 같이 성공적으로 커널 모듈이 로드되었음을 알 수 있다.


juntae@ubuntu:~/kernel/ioctl$ lsmod | grep "chardev"
chardev                16384  0

마지막으로 해당 커널 모듈을 사용하기위해 test.c파일을 만들어 컴파일한다.


juntae@ubuntu:~/kernel/ioctl$ gcc -o test test.c 

그리고 해당 프로그램을 실행시킨다.

나는 유저권한으로 프로그램을 실행시켰을때 안돼서 sudo명령을 사용했다.


juntae@ubuntu:~/kernel/ioctl$ sudo ./test 
get_info.size : 100, get_info.buf : lazenca.0x0

잘 실행되는것을 볼 수 있다.

이렇게 커널 영역에서 프로그램이 실행되면 dmesg에 로그가 기록된다.


커널 메시지는 여러부분에 기록이 남는다.

/bin/dmesg : 실행 파일 위치
/var/log/dmesg : 부팅하는 동안 커널의 기록을 남겨 놓는 파일
dmesg : 부팅 후의 커널에 콘트롤 되는 메시지를 ring buffer에 저장되는 메시지
/var/log/messages : 부팅 후 시스템에 관련된 로그 파일이 기록되는 파일

로그를 직접 남기고싶으면 printk함수를 사용하면 된다.


혹시 프로그램 실행 도중 에러로 프로그램이 종료된다면, dmesg 를 적극 활용하여 kernel 의 어느 부분때문에 종료가 되었는지 디버깅을 해보자.

이번에는 커널 메시지를 출력해주는 명령어인 dmesg명령을 사용해보자.

juntae@ubuntu:~/kernel/ioctl$ dmesg | tail
[16770.869667] info.size : 100, info.buf : lazenca.0x0 \xfffffff1
[16770.869668] The chardev_ioctl() function has been called.
[16770.869669] GET_DATA
[16770.869787] The chardev_close() function has been called.
[16776.372237] The chardev_open() function has been called.
[16776.372240] The chardev_ioctl() function has been called.
[16776.372241] SET_DATA
[16776.372242] info.size : 100, info.buf : lazenca.0x0 \xfffffff0
[16776.372243] The chardev_ioctl() function has been called.
[16776.372244] GET_DATA

위와 같이 커널에서 해당 프로그램이 잘 작동했음을 알 수 있다.


Reference