天天看點

Linux 字元裝置驅動開發基礎(四)—— ioctl() 函數解析

        解析完 open、close、read、write 四個函數後,終于到我們的 ioctl() 函數了

一、 什麼是ioctl

         ioctl是裝置驅動程式中對裝置的I/O通道進行管理的函數。所謂對I/O通道進行管理,就是對裝置的一些特性進行控制,例如序列槽的傳輸波特率、馬達的轉速等等。下面是其源代碼定義:

函數名: ioctl

功 能: 控制I/O裝置

用 法: int ioctl(int handle, int cmd,[int *argdx, int argcx]);

參數:fd是使用者程式打開裝置時使用open函數傳回的檔案标示符,cmd是使用者程式對裝置的控制指令,後面是一些補充參數,一般最多一個,這個參數的有無和cmd的意義相關。

include/asm/ioctl.h中定義的宏的注釋:

#define   _IOC_NRBITS        8                               //序數(number)字段的字位寬度,8bits
#define   _IOC_TYPEBITS      8                               //幻數(type)字段的字位寬度,8bits
#define   _IOC_SIZEBITS      14                              //大小(size)字段的字位寬度,14bits
#define   _IOC_DIRBITS       2                               //方向(direction)字段的字位寬度,2bits
#define   _IOC_NRMASK       ((1 << _IOC_NRBITS)-1)    //序數字段的掩碼,0x000000FF
#define   _IOC_TYPEMASK     ((1 << _IOC_TYPEBITS)-1)  //幻數字段的掩碼,0x000000FF
#define   _IOC_SIZEMASK     ((1 << _IOC_SIZEBITS)-1)   //大小字段的掩碼,0x00003FFF
#define   _IOC_DIRMASK      ((1 << _IOC_DIRBITS)-1)    //方向字段的掩碼,0x00000003
#define   _IOC_NRSHIFT     0                                //序數字段在整個字段中的位移,0
#define   _IOC_TYPESHIFT   (_IOC_NRSHIFT+_IOC_NRBITS)       //幻數字段的位移,8
#define   _IOC_SIZESHIFT   (_IOC_TYPESHIFT+_IOC_TYPEBITS)   //大小字段的位移,16
#define   _IOC_DIRSHIFT    (_IOC_SIZESHIFT+_IOC_SIZEBITS)   //方向字段的位移,30
#define _IOC_NONE    0U     //沒有資料傳輸
#define _IOC_WRITE   1U     //向裝置寫入資料,驅動程式必須從使用者空間讀入資料
#define _IOC_READ    2U     //從裝置中讀取資料,驅動程式必須向使用者空間寫入資料
#define _IOC(dir,type,nr,size) \
       (((dir)  << _IOC_DIRSHIFT) | \
        ((type) << _IOC_TYPESHIFT) | \
        ((nr)   << _IOC_NRSHIFT) | \
        ((size) << _IOC_SIZESHIFT))
 
//構造無參數的指令編号
#define _IO(type,nr)             _IOC(_IOC_NONE,(type),(nr),0)
//構造從驅動程式中讀取資料的指令編号
#define _IOR(type,nr,size)     _IOC(_IOC_READ,(type),(nr),sizeof(size))
//用于向驅動程式寫入資料指令
#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
//用于雙向傳輸
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
 
//從指令參數中解析出資料方向,即寫進還是讀出
#define _IOC_DIR(nr)          (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
//從指令參數中解析出幻數type
#define _IOC_TYPE(nr)              (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
//從指令參數中解析出序數number
#define _IOC_NR(nr)           (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
//從指令參數中解析出使用者資料大小
#define _IOC_SIZE(nr)         (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)


#define IOC_IN            (_IOC_WRITE << _IOC_DIRSHIFT)
#define IOC_OUT           (_IOC_READ << _IOC_DIRSHIFT)
#define IOC_INOUT         ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
#define IOCSIZE_MASK      (_IOC_SIZEMASK << _IOC_SIZESHIFT)
#define IOCSIZE_SHIFT     (_IOC_SIZESHIFT)
           

二、ioctl的必要性 

       如果不用ioctl的話,也可以實作對裝置I/O通道的控制。例如,我們可以在驅動程式中實作write的時候檢查一下是否有特殊約定的資料流通過,如果有的話,那麼後面就跟着控制指令(一般在socket程式設計中常常這樣做)。但是如果這樣做的話,會導緻代碼分工不明,程式結構混亂,程式員自己也會頭昏眼花的。是以,我們就使用ioctl來實作控制的功能。要記住,使用者程式所作的隻是通過指令碼(cmd)告訴驅動程式它想做什麼,至于怎麼解釋這些指令和怎麼實作這些指令,這都是驅動程式要做的事情。

三、 ioctl如何實作

        在驅動程式中實作的ioctl函數體内,實際上是有一個switch{case}結構,每一個case對應一個指令碼,做出一些相應的操作。怎麼實作這些操作,這是每一個程式員自己的事情。因為裝置都是特定的,這裡也沒法說。關鍵在于怎樣組織指令碼,因為在ioctl中指令碼是唯一聯系使用者程式指令和驅動程式支援的途徑。

        指令碼的組織是有一些講究的,因為我們一定要做到指令和裝置是一一對應的,這樣才不會将正确的指令發給錯誤的裝置,或者是把錯誤的指令發給正确的裝置,或者是把錯誤的指令發給錯誤的裝置。這些錯誤都會導緻不可預料的事情發生,而當程式員發現了這些奇怪的事情的時候,再來調試程式查找錯誤,那将是非常困難的事情。是以在Linux核心中是這樣定義一個指令碼的: 

| 裝置類型 | 序列号 | 方向 |資料尺寸|

|-------------|----------|-------|------------|

|     8 bit     |   8 bit  | 2 bit |  8~14 bit |

|-------------|----------|-------|-------------|

     這樣一來,一個指令就變成了一個整數形式的指令碼;但是指令碼非常的不直覺,是以Linux Kernel中提供了一些宏。這些宏可根據便于了解的字元串生成指令碼,或者是從指令碼得到一些使用者可以了解的字元串以标明這個指令對應的裝置類型、裝置序列号、資料傳送方向和資料傳輸尺寸。

比如上面展現的:

//構造無參數的指令編号
#define _IO(type,nr)             _IOC(_IOC_NONE,(type),(nr),0)
//構造從驅動程式中讀取資料的指令編号
#define _IOR(type,nr,size)     _IOC(_IOC_READ,(type),(nr),sizeof(size))
//用于向驅動程式寫入資料指令
#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
//用于雙向傳輸
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
           

    我們在前面PWM驅動程式中也定義了指令宏:

#define   MAGIC_NUMBER    'k'
#define   BEEP_ON    _IO(MAGIC_NUMBER    ,0)
#define   BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
#define   BEEP_FREQ  _IO(MAGIC_NUMBER    ,2)
           

     這裡必須要提一下的,就是"幻數"MAGIC_NUMBER, "幻數"是一個字母,資料長度也是8,用一個特定的字母來标明裝置類型,這和用一個數字是一樣的,隻是更加利于記憶和了解。

四、 cmd參數如何得出 

    這裡确實要說一說,cmd參數在使用者程式端由一些宏根據裝置類型、序列号、傳送方向、資料尺寸等生成,這個整數通過系統調用傳遞到核心中的驅動程式,再由驅動程式使用解碼宏從這個整數中得到裝置的類型、序列号、傳送方向、資料尺寸等資訊,然後通過switch{case}結構進行相應的操作。

執行個體時刻,當然隻是部分代碼:

#define  MAGIC_NUMBER    'k'
#define  BEEP_ON    _IO(MAGIC_NUMBER    ,0)
#define  BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
#define  BEEP_FREQ  _IO(MAGIC_NUMBER    ,2)
#define  BEPP_IN_FREQ 100000

static void beep_freq(unsigned long arg)
{
	writel(BEPP_IN_FREQ/arg, timer_base +TCNTB0  );
	writel(BEPP_IN_FREQ/(2*arg), timer_base +TCMPB0 );
}

static long beep_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
	switch(cmd)
	{
		case BEEP_ON:
			fs4412_beep_on();
			break;
		case BEEP_OFF:
			fs4412_beep_off();
			break;
		case BEEP_FREQ:
			beep_freq( arg );
			break;
		default :
			return -EINVAL;
	}
}
           

測試代碼如下:

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

#define  MAGIC_NUMBER    'k'
#define   BEEP_ON    _IO(MAGIC_NUMBER    ,0)
#define   BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
#define   BEEP_FREQ   _IO(MAGIC_NUMBER    ,2)

main()
{
	int fd;

	fd = open("/dev/beep",O_RDWR);
	if(fd<0)
	{
		perror("open fail \n");
		return ;
	}

	ioctl(fd,BEEP_ON);

	sleep(6);
	ioctl(fd,BEEP_OFF);	

	close(fd);
}
           

繼續閱讀