一不小心放了太多東西進去了,不過多看一點也不錯。user space的process會先發出一個system call,例如read, write,好奇system call是怎麼被定義的嗎?其實他們長這樣(參數對應process裡傳給read和write的參數):
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
在system call裡面會根據傳入的fd找到對應的file資料結構,再調用檔案操作結構裡面的read或write。
以上圖來看,我們目前的位置在charecter device的地方,下面那個other device是...?那個就是依附在bus下的裝置啦,他們沒辦法以一個檔案的方式出現在/dev目錄下,所以只好請求字元裝置來幫忙囉!他們也會提供一些callback functions,字元裝置就可以利用這些callback functions存取依附在bus下的裝置了。
上圖中的file-system就是常見的ext3,ubifs...這些檔案系統啦,他們跟block device之間的資料傳輸是透過request的方式,以後如果有機會在看看block device的部份。socket-protocol(TCP, UDP, IP)-net_device(網路裝置)之間溝通是透過sk_buf的資料結構,經過一個protocol就在在資料上加上一個頭部,最後透過網路裝置送出去,封包收進來後就一層一層的把頭部摘掉。
嗯...好像扯太遠了,把話題拉回檔案操作結構,先來看看我們的檔案操作結構長什麼樣吧。
static struct file_operations chrdev_fops = {
.read = chrdev_read,
.write = chrdev_write,
.unlocked_ioctl = chrdev_ioctl,
.mmap = chrdev_mmap,
.open = chrdev_open,
.release = chrdev_release,
};
其實整個完整的結構裡面的callback function有很多,可是有些並不一定會用到,所以我們根據想要的功能只實現其中的某一部份。接下來就一一來探討這些callback function做了哪些事。
首先來看看chrdev_open()和chrdev_release()
int chrdev_open(struct inode *inode, struct file *filp)
{
int id = iminor(inode);
struct chrdev_inst *inst;
if (id == c_dev.id) {
inst = dev_get_drvdata(c_dev.dev);
filp->private_data = inst;
return 0;
} else {
return -ENODEV;
}
}
int chrdev_release(struct inode *inode, struct file *filp)
{
return 0;
}
在linux的世界裡,每個檔案就是一個inode,可以藉由inode區分這個檔案是字元裝置(i_cdev)還是區塊裝置(i_bdev),同時他還會紀錄裝置編號(i_rdev),還記得MKDEV函式產生裝置編號時傳入的次編號是什麼嗎?沒錯!就是字元裝置的ID,所以可以用iminor()來獲取裝置的ID。
因為沒有把裝置的結構(inst)宣告成全域變數,只有在建立device時把他當成私有資料傳入,dev_get_drvdata()就是用來得到私有資料,接著再把他當成file結構的私有資料,這樣作的原因是之後在read,write,ioctl...裡都會傳入file的結構,可以利用這個方法直接取得裝置的結構(inst)。在open()函式裡沒有分配記憶體,也沒有註冊IRQ,所以release()其實沒事可作。
接下來看看chrdev_read()和chrdev_write()
static ssize_t chrdev_read(struct file *filp, char __user *buff, size_t count, loff_t *ppos)
{
int ret;
struct chrdev_inst *inst = filp->private_data;
if(count > inst->size)
count = inst->size;
if (copy_to_user(buff, inst->buf, count))
ret = -EFAULT;
else
ret = count;
return ret;
}
static ssize_t chrdev_write(struct file *filp, const char __user *buff, size_t count, loff_t *ppos)
{
int ret;
struct chrdev_inst *inst = filp->private_data;
if (count > inst->size)
count = inst->size;
if (copy_from_user(inst->buf, buff, count))
ret = -EFAULT;
else
ret = count;
return ret;
}
這兩個函式會收到一樣的參數,一個指向user space資料的指標(buff),資料的大小(count)和目前的位置(*ppos),目前的位置在這邊並沒有被用到,也就是說每次讀寫都是從第0個byte開始寫或從第0個byte開始讀。因為user space和kernel space之間的記憶體空間不能夠互相直接存取,所以必須藉由copy_to_user()和copy_from_user()複製資料。如果成功讀寫就回傳資料的大小。
接著是chrdev_mmap()的部份
static int chrdev_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret = 0;
unsigned long len = vma->vm_end - vma->vm_start;
unsigned long offset = vma->vm_pgoff<<PAGE_SHIFT;
struct chrdev_inst *inst = filp->private_data;
if (offset > inst->size)
return -EINVAL;
if ((len+offset) > inst->size)
return -EINVAL;
offset = virt_to_phys(offset + inst->buf) >> PAGE_SHIFT;
if (remap_pfn_range(vma, vma->vm_start, offset, len, vma->vm_page_prot))
ret = -EAGAIN;
return ret;
}
在看這個函式之前,我們先來看看在user space是怎麼呼叫的
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr指的是希望map到的virtual address,不過通常都是0,由kernel自己分配。len是map的檔案長度,以byte為單位。prot是這個區間可讀或可寫,如果有多個process同時map相同的檔案,flag標示如果檔案被一方修改另一方式否可見。offset指的是從檔案的哪裡開始map,必須是PAGE_SIZE大小的整數倍,PAGE_SIZE大小應系統而異,在我的電腦上是4KB。 當user space呼叫mmap()時,會先分配一塊virtual address,實際map到physical address是靠檔案操作函式的mmap()完成。
回到檔案操作函式的mmap(),vm_area_struct結構紀錄的相關的資訊,結構成員vm_start和vm_end是在virtual address開始和結束的位址,vm_pgoff是offset,不過是以PAGE為單位,所以需要往左shift PAGE_SHIFT(12)的長度。
接下來就是利用remap_pfn_range()把我們的裝置記憶體從offset的地方map一個len長度的大小給user space了,傳入的offset參數必須是實體記憶體對應的page frame number,所以要透過virt_to_phys()轉成實體記憶體後再往右shift PAGE_SHIFT。
最後就是chrdev_ioctl()了
#define CHRDEV_TYPE 'k'
#define CHRDEV_ERASE _IO(CHRDEV_TYPE, 0x50)
#define CHRDEV_RDWR _IO(CHRDEV_TYPE, 0x51)
struct chrdev_msg {
int flag;
#define MSG_WR 1
#define MSG_RD 2
unsigned int from;
unsigned int len;
char *buf;
};
struct chrdev_ioctl_data {
int num_msg;
struct chrdev_msg *msg;
};
static int chrdev_ioctl_rdwr(struct file *filp, unsigned long arg)
{
int i, j, ret;
struct chrdev_ioctl_data chrdev_data;
struct chrdev_msg *msg;
char *buffer[MAX_NUM_MSG];
struct chrdev_inst *inst = filp->private_data;
if (copy_from_user(&chrdev_data, (struct chrdev_ioctl_data*)arg, sizeof(chrdev_data)))
return -EFAULT;
msg = kzalloc(chrdev_data.num_msg*sizeof(struct chrdev_msg), GFP_KERNEL);
if (msg == NULL)
return -ENOMEM;
if (copy_from_user(msg, chrdev_data.msg, chrdev_data.num_msg*sizeof(struct chrdev_msg))) {
kfree(msg);
return -EFAULT;
}
for (i=0; i<chrdev_data.num_msg; i++) {
buffer[i] = kzalloc(msg[i].len, GFP_KERNEL);
if (buffer[i] == NULL)
break;
if (msg[i].flag & MSG_WR) {
if (copy_from_user(buffer[i], msg[i].buf, msg[i].len)) {
kfree(buffer[i]);
break;
}
}
}
for (j = 0; j < i; j++) {
int from = msg[j].from;
int len = msg[j].len;
if (from > inst->size) {
ret = -EINVAL;
goto free_all;
}
if ((len+from) > inst->size)
len = inst->size - from;
if (msg[j].flag & MSG_RD) {
memcpy(buffer[j], (inst->buf+from), msg[j].len);
if (copy_to_user(msg[j].buf, buffer[j], msg[j].len))
break;
} else
memcpy((inst->buf+from), buffer[j], msg[j].len);
}
ret = j;
free_all:
for (j=0; j<i; j++)
kfree(buffer[j]);
kfree(msg);
return ret;
}
static long chrdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct chrdev_inst *inst = filp -> private_data;
switch(cmd) {
case CHRDEV_RDWR:
ret = chrdev_ioctl_rdwr(filp, arg);
break;
case CHRDEV_ERASE:
memset(inst->buf, 0, BUFFER_SIZE);
break;
default:
return -EINVAL;
}
return 0;
}
這個函式會接收一個命令(cmd)和一個參數(arg),參數的部份可以自行定義,不過process和裝置驅動都要對這個參數有共識,型態轉換的時候才不會出錯。命令的部份就不能隨便定義了,因為有可能會跟系統中已有的命令相衝突,來看看kernel裡面是怎麼做的吧!
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
struct file *filp;
int error = -EBADF;
int fput_needed;
filp = fget_light(fd, &fput_needed);
if (!filp)
goto out;
error = security_file_ioctl(filp, cmd, arg);
if (error)
goto out_fput;
error = do_vfs_ioctl(filp, fd, cmd, arg);
out_fput:
fput_light(filp, fput_needed);
out:
return error;
}
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd, unsigned long arg)
{
int error = 0;
int __user *argp = (int __user *)arg;
switch (cmd) {
case FIOCLEX:
set_close_on_exec(fd, 1);
break;
case FIONCLEX:
set_close_on_exec(fd, 0);
break;
case FIONBIO:
error = ioctl_fionbio(filp, argp);
break;
case FIOASYNC:
error = ioctl_fioasync(fd, filp, argp);
break;
case FIOQSIZE:
if (S_ISDIR(filp->f_path.dentry->d_inode->i_mode) ||
S_ISREG(filp->f_path.dentry->d_inode->i_mode) ||
S_ISLNK(filp->f_path.dentry->d_inode->i_mode)) {
loff_t res =
inode_get_bytes(filp->f_path.dentry->d_inode);
error = copy_to_user((loff_t __user *)arg, &res,
sizeof(res)) ? -EFAULT : 0;
} else
error = -ENOTTY;
break;
case FIFREEZE:
error = ioctl_fsfreeze(filp);
break;
case FITHAW:
error = ioctl_fsthaw(filp);
break;
case FS_IOC_FIEMAP:
return ioctl_fiemap(filp, arg);
case FIGETBSZ:
{
struct inode *inode = filp->f_path.dentry->d_inode;
int __user *p = (int __user *)arg;
return put_user(inode->i_sb->s_blocksize, p);
}
default:
if (S_ISREG(filp->f_path.dentry->d_inode->i_mode))
error = file_ioctl(filp, cmd, arg);
else
error = vfs_ioctl(filp, cmd, arg);
break;
}
return error;
}
chrdev_ioctl()要到vfs_ioctl()這邊才會被調用,在這之前命令會被用來判斷是否是FIOCLEX,FIONCLEX,...,FIGETBSZ這些情況,至少我們的命令不可以跟這些衝突,不過保險起見還是參考一下Documentation/ioctl/ioctl-number.txt裡面所列已經被使用的命令。要怎麼設定一個命令呢?kernel提供了幾個方法:
_IO(type, nr)
_IOR(typr, nr, size)
_IOW(type, nr, size)
_IORW(type, nr, size)
可是...這樣在process裡面怎麼知道命令是什麼?嗯...這真是個好問題,今天就先看看ioctl()裡面在做些什麼,這個問題就留到下次探討吧!
chrdev_ioctl()裡面只做兩件事,一個是user space的process透過chrdev_ioctl_data這個資料結構存取裝置的記憶體,令一個就是清除裝置的記憶體內容。chrdev_ioctl_data這是自己定義的一個結構,成員有chrdev_msg結構的數量(num_msg)和chrdev_msg結構的指標(msg),也可以看成一個陣列。而chrdev_msg結構可以把他看成一個message,他的成員有flag(這個message是讀或寫),from(從裝置記憶體哪裡開始讀或寫),len(讀或寫的大小)和一個指向user space的記憶體指標(buf)。
實際實現這個方法是在chrdev_ioctl_rdwr()這個函式裡面,他會根據chrdev_ioctl_data和chrdev_msg裡面的欄位分配message和buffer的記憶體大小,並且把需要的資料從user space複製到kernel space來,最後就是對裝置的記憶體作讀寫了。如果在處理某個message上出錯了,之前成功的message還是可以被用來讀寫,並且回傳成功的message的數量。
那如果process同時傳來兩個message都是寫資料到一樣的裝置記憶體位址,有沒有需要作錯誤處理呢?這個問題就牽涉到策略(policy)的部份,而linux開發者希望把有關策略的部份放到user space處理,kernel裡面只留能力(ability)或功能(functionality)的部份。udev也是因為這個原因才被開發出來的。
有關這個字元裝置的檔案作函式大概就介紹到這邊啦!如果覺得意猶未盡,想要了解更多其他的操作函式,可以參考kernel裡面其他driver的作法,或是上google找找囉!
沒有留言:
張貼留言