1. 程式人生 > >Linux應用程式訪問字元裝置驅動詳細過程解析

Linux應用程式訪問字元裝置驅動詳細過程解析

下面先通過一個編寫好的核心驅動模組來體驗以下字元裝置驅動

可以暫時先忽略下面的程式碼實現!

memdev.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>

int dev1_registers[5];
int dev2_registers[5];

struct cdev cdev; 
dev_t devno;

/*檔案開啟函式*/
int mem_open(struct inode *inode, struct file *filp)
{
    
    /*獲取次裝置號*/
    int num = MINOR(inode->i_rdev);
    
    if (num==0)
        filp->private_data = dev1_registers;
    else if(num == 1)
        filp->private_data = dev2_registers;
    else
        return -ENODEV;  //無效的次裝置號
    
    return 0; 
}

/*檔案釋放函式*/
int mem_release(struct inode *inode, struct file *filp)
{
  return 0;
}

/*讀函式*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  int *register_addr = filp->private_data; /*獲取裝置的暫存器基地址*/

  /*判斷讀位置是否有效*/
  if (p >= 5*sizeof(int))
    return 0;
  if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;

  /*讀資料到使用者空間*/
  if (copy_to_user(buf, register_addr+p, count))
  {
    ret = -EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;
  }

  return ret;
}

/*寫函式*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  int *register_addr = filp->private_data; /*獲取裝置的暫存器地址*/
  
  /*分析和獲取有效的寫長度*/
  if (p >= 5*sizeof(int))
    return 0;
  if (count > 5*sizeof(int) - p)
    count = 5*sizeof(int) - p;
    
  /*從使用者空間寫入資料*/
  if (copy_from_user(register_addr + p, buf, count))
    ret = -EFAULT;
  else
  {
    *ppos += count;
    ret = count;
  }

  return ret;
}

/* seek檔案定位函式 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{ 
    loff_t newpos;

    switch(whence) {
      case SEEK_SET: 
        newpos = offset;
        break;

      case SEEK_CUR: 
        newpos = filp->f_pos + offset;
        break;

      case SEEK_END: 
        newpos = 5*sizeof(int)-1 + offset;
        break;

      default: 
        return -EINVAL;
    }
    if ((newpos<0) || (newpos>5*sizeof(int)))
    	return -EINVAL;
    	
    filp->f_pos = newpos;
    return newpos;

}

/*檔案操作結構體*/
static const struct file_operations mem_fops =
{
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .release = mem_release,
};

/*裝置驅動模組載入函式*/
static int memdev_init(void)
{
  /*初始化cdev結構*/
  cdev_init(&cdev, &mem_fops);
  
  /* 註冊字元裝置 */
  alloc_chrdev_region(&devno, 0, 2, "memdev");
  cdev_add(&cdev, devno, 2);
}

/*模組解除安裝函式*/
static void memdev_exit(void)
{
  cdev_del(&cdev);   /*登出裝置*/
  unregister_chrdev_region(devno, 2); /*釋放裝置號*/
}

MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);


1. 編譯/安裝驅動:在Linux系統中,驅動程式通常採用核心模組的程式結構來進行編碼,因此編譯、安裝一個驅動程式,其實質就是編譯/安裝一個核心模組。

2. 建立裝置檔案


應用程式如何通過字元裝置檔案來訪問裝置驅動介面,即字元裝置驅動程式訪問大揭祕,下面先來看個應用程式的小例子:(應用程式是如何通過系統呼叫找到裝置驅動程式入口的然後讓裝置驅動程式work

read-mem.c

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

int main()
{
	int fd = 0;
	int dst = 0;
	
	/*開啟裝置檔案*/
	fd = open("/dev/memdev0",O_RDWR);
	
	/*寫入資料*/
	read(fd, &dst, sizeof(int));
	
	printf("dst is %d\n",dst);
	
	/*關閉裝置*/
	close(fd);
	
	return 0;	

}


在linux下對上面的檔案進行靜態編譯(考慮到前面開發板上移植的某些庫還沒有新增進去)生成read-mem目標檔案,然後進行反彙編並將反彙編生成的檔案匯入到當前目錄下的dump上去。

VIM開啟dump檔案,搜尋/main 可以看到這一段彙編程式碼


可以看到 應用程式中的read()函式實際是呼叫了__libc_read函式, 然後繼續在dump中搜索__libc_read函式


這裡紅箭頭指向的兩行是比較重要的兩行,將3傳給r7,然後使用了SVC系統呼叫指令,這時PC指標會從使用者空間進入到核心空間(通過一個固定的入口),第二步會取r7暫存器裡面的值3, 然後根據這個值查一個表確定要呼叫那個系統呼叫(即對於3的系統呼叫核心程式碼)。這裡開啟核心原始碼工程,開啟一個檔案:

entry-comon.S(/arch/arm/kernel目錄下)


上面的核心程式碼部分vector_swi就是那個固定的入口,第二個箭頭部分就是上面所說的第二步,第三步在這部分的程式碼下面這裡截圖截不了這麼多(也就是根據number查表)

enable_irq

	get_thread_info tsk
	adr	tbl, <span style="color:#ff0000;">sys_call_table</span>		@ load syscall table pointer
搜尋sys_call_table,來看看這張表是什麼?


檢視calls.S檔案


系統就是通過固定入口進入核心空間,然後取出系統呼叫編號,在利用編號查詢上面的這張表,然後取出核心中對於上面使用者空間的read的實現函式!(這裡就分析了使用者空間的read是如何找到核心空間的sys_read的過程

這裡順便看一下sys_read的核心程式碼實現:/fs 目錄下的 Read_write.c檔案中

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
	struct file *file;
	ssize_t ret = -EBADF;
	int fput_needed;

	file = fget_light(fd, &fput_needed);
	if (file) {
		loff_t pos = file_pos_read(file);
		ret = vfs_read(file, buf, count, &pos);
		file_pos_write(file, pos);
		fput_light(file, fput_needed);
	}

	return ret;
}
每個開啟的檔案都會有一個struct file與之對應!從上面的函式可以看到通過傳入的fd引數 能找到與之對應的struct file.

然後通過file 呼叫了vfs_read()函式.下面先看看該函式的內部實現。



紅色箭頭部分!f_op部分是驅動程式裡面的自定義結構,通過f_op結構找到裝置讀取方法!

就是這個:

/*檔案操作結構體*/
static const struct file_operations mem_fops =
{
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .release = mem_release,
};