關(guān)于Linux文件系統(tǒng)的幾點(diǎn)注意事項(xiàng)
掃描二維碼
隨時(shí)隨地手機(jī)看文章
做內(nèi)核開(kāi)發(fā)的朋友,可能對(duì)下面的代碼都很眼熟。
1. static const struct file_operations xxx_fops = {
2. .owner = THIS_MODULE,
3. .llseek = no_llseek,
4. .write = xxx_write,
5. .unlocked_ioctl = xxx_ioctl,
6. .open = xxx_open,
7. .release = xxx_release,
8. };
一般我們?cè)趚xx_open中會(huì)用類似如下的代碼分配一塊內(nèi)存。
[cpp] view plain copy
1. file->private_data = kmalloc(sizeof(struct xxx), GFP_KERNEL);
然后在接下來(lái)的read/write/ioctl中,我們就可以通過(guò)file->private_data取到與此文件關(guān)聯(lián)的數(shù)據(jù)。
最后,在xxx_release中,我們會(huì)釋放file->private_data指向的內(nèi)存。
如果只是上面這幾種流程訪問(wèn)file->private_data所指向的數(shù)據(jù),基本上不會(huì)出問(wèn)題。
因?yàn)閮?nèi)核的文件系統(tǒng)框架已經(jīng)做了很完善的處理。
對(duì)于迸發(fā)訪問(wèn),我們自己也可以通過(guò)鎖等機(jī)制來(lái)解決。
然而,我們通常還會(huì)在一些異步的流程中訪問(wèn)file->private_data所指向的數(shù)據(jù),這些異步流程可能由定時(shí)器,中斷,進(jìn)程間通信等因素觸發(fā)。
并且,這些流程訪問(wèn)數(shù)據(jù)時(shí),沒(méi)有經(jīng)過(guò)內(nèi)核的文件系統(tǒng)框架。
那么這就有可能導(dǎo)致出現(xiàn)問(wèn)題了。
下面我們先來(lái)看看內(nèi)核文件系統(tǒng)框架的部分實(shí)現(xiàn)代碼,再來(lái)考慮如何規(guī)避可能出現(xiàn)的問(wèn)題。我們的分析基于linux-3.10.102的內(nèi)核源碼。
首先,要得到一個(gè)fd,必須先有一次調(diào)用C庫(kù)函數(shù)open的行為。而在C庫(kù)函數(shù)open返回之前,其他線程得不到fd,當(dāng)然也就不會(huì)對(duì)此fd進(jìn)行操作。等拿到fd時(shí),open操作都已經(jīng)完成了。
實(shí)際上,更夸張的情況還是有可能存在的。例如,可能由于程序的錯(cuò)誤甚至是程序員故意構(gòu)造特殊代碼,導(dǎo)致在open返回之前,其他線程就使用即將返回的fd進(jìn)行文件操作了。這種情況,這里就不討論了。有興趣的朋友,可以自己鉆研內(nèi)核代碼,看看會(huì)產(chǎn)生什么效果。
先看看文件打開(kāi)操作的主要函數(shù)調(diào)用:
sys_open, do_sys_open, do_filp_open, fd_install, __fd_install。
安裝fd的操作如下??梢?jiàn)這里是對(duì)文件表加了鎖的,并且不是針對(duì)單個(gè)文件,是整體性的加鎖。
[cpp] view plain copy
1. void __fd_install(struct files_struct *files, unsigned int fd,
2. struct file *file)
3. {
4. struct fdtable *fdt;
5. spin_lock(&files->file_lock);
6. fdt = files_fdtable(files);
7. BUG_ON(fdt->fd[fd] != NULL);
8. rcu_assign_pointer(fdt->fd[fd], file);
9. spin_unlock(&files->file_lock);
10. }
讀寫(xiě)操作,代碼結(jié)構(gòu)非常相似。這里只看寫(xiě)操作吧。其實(shí)現(xiàn)如下:
[cpp] view plain copy
1. SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
2. size_t, count)
3. {
4. struct fd f = fdget(fd);
5. ssize_t ret = -EBADF;
6.
7. if (f.file) {
8. loff_t pos = file_pos_read(f.file);
9. ret = vfs_write(f.file, buf, count, &pos);
10. file_pos_write(f.file, pos);
11. fdput(f);
12. }
13.
14. return ret;
15. }
[cpp] view plain copy
1. ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
2. {
3. ssize_t ret;
4.
5. if (!(file->f_mode & FMODE_WRITE))
6. return -EBADF;
7. if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
8. return -EINVAL;
9. if (unlikely(!access_ok(VERIFY_READ, buf, count)))
10. return -EFAULT;
11.
12. ret = rw_verify_area(WRITE, file, pos, count);
13. if (ret >= 0) {
14. count = ret;
15. file_start_write(file);
16. if (file->f_op->write)
17. ret = file->f_op->write(file, buf, count, pos);
18. else
19. ret = do_sync_write(file, buf, count, pos);
20. if (ret > 0) {
21. fsnotify_modify(file);
22. add_wchar(current, ret);
23. }
24. inc_syscw(current);
25. file_end_write(file);
26. }
27.
28. return ret;
29. }
[cpp] view plain copy
1. ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
2. {
3. struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
4. struct kiocb kiocb;
5. ssize_t ret;
6.
7. init_sync_kiocb(&kiocb, filp);
8. kiocb.ki_pos = *ppos;
9. kiocb.ki_left = len;
10. kiocb.ki_nbytes = len;
11.
12. ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
13. if (-EIOCBQUEUED == ret)
14. ret = wait_on_sync_kiocb(&kiocb);
15. *ppos = kiocb.ki_pos;
16. return ret;
17. }
可以看出,讀寫(xiě)操作是無(wú)鎖的。也不好加鎖,因?yàn)樽x寫(xiě)操作,還有ioctl,有可能阻塞。如果需要鎖,用戶自己可以使用文件鎖,《UNIX環(huán)境高級(jí)編程》中有關(guān)于文件鎖的描述。
不過(guò)fdget與fdput中包含了一些rcu方面的操作,那是為了能夠與close fd的操作迸發(fā)進(jìn)行。[!--empirenews.page--]
另外,可以看出,如果只實(shí)現(xiàn)一個(gè)f_op->aio_write,也是可以支持C庫(kù)函數(shù)write的。
再來(lái)看看ioctl的實(shí)現(xiàn)。
[cpp] view plain copy
1. SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
2. {
3. int error;
4. struct fd f = fdget(fd);
5.
6. if (!f.file)
7. return -EBADF;
8. error = security_file_ioctl(f.file, cmd, arg);
9. if (!error)
10. error = do_vfs_ioctl(f.file, fd, cmd, arg);
11. fdput(f);
12. return error;
13. }
對(duì)于非常規(guī)文件,或者常規(guī)文件中文件系統(tǒng)特有的命令,最終都會(huì)走到
filp->f_op->unlocked_ioctl
另外,ioctl也是無(wú)鎖的。同時(shí),流程中包含了fdget與fdput,這一點(diǎn)與read/write一樣。
再來(lái)看看關(guān)閉文件的操作。系統(tǒng)調(diào)用sys_close的實(shí)現(xiàn)如下(fs/open.c)
[cpp] view plain copy
1. SYSCALL_DEFINE1(close, unsigned int, fd)
2. {
3. int retval = __close_fd(current->files, fd);
4.
5. /* can‘t restart close syscall because file table entry was cleared */
6. if (unlikely(retval == -ERESTARTSYS ||
7. retval == -ERESTARTNOINTR ||
8. retval == -ERESTARTNOHAND ||
9. retval == -ERESTART_RESTARTBLOCK))
10. retval = -EINTR;
11.
12. return retval;
13. }
可見(jiàn)主要工作是__close_fd函數(shù)(fs/file.c)完成的,其代碼如下??梢?jiàn)他是對(duì)進(jìn)程的文件表加了鎖的。因此,open、close操作是有互斥的,并且不是針對(duì)某一文件的互斥,而是整體的互斥。
對(duì)于close一個(gè)fd時(shí),其他cpu上的線程若正要或正在讀寫(xiě)此fd怎么辦?可以看出,close操作并不會(huì)為此等待,而是直接繼續(xù)操作。
其中的rcu_assign_pointer(fdt->fd[fd], NULL);清除了此fd與file結(jié)構(gòu)的關(guān)聯(lián),因此在此之后通過(guò)此fd已經(jīng)訪問(wèn)不到相應(yīng)的file結(jié)構(gòu)了。至于在此之前就發(fā)起了的且尚未結(jié)束的訪問(wèn)怎么處理,答案是在filp_close中處理。
[cpp] view plain copy
1. int __close_fd(struct files_struct *files, unsigned fd)
2. {
3. struct file *file;
4. struct fdtable *fdt;
5.
6. spin_lock(&files->file_lock);
7. fdt = files_fdtable(files);
8. if (fd >= fdt->max_fds)
9. goto out_unlock;
10. file = fdt->fd[fd];
11. if (!file)
12. goto out_unlock;
13. rcu_assign_pointer(fdt->fd[fd], NULL);
14. __clear_close_on_exec(fd, fdt);
15. __put_unused_fd(files, fd);
16. spin_unlock(&files->file_lock);
17. return filp_close(file, files);
18.
19. out_unlock:
20. spin_unlock(&files->file_lock);
21. return -EBADF;
22. }
filp_close又調(diào)用了fput, 后者的相關(guān)代碼如下??梢?jiàn)當(dāng)前任務(wù)若非內(nèi)核線程,接下來(lái)就是走_(dá)___fput,否則就是走delayed_fput。
但是最終都是走_(dá)_fput,__fput中會(huì)調(diào)用file->f_op->release,即我們的xxx_release。
不過(guò),從fput代碼可以看出,____fput會(huì)由rcu相關(guān)的work觸發(fā)。因此,可以預(yù)見(jiàn)當(dāng)____fput被調(diào)用時(shí),已經(jīng)沒(méi)有已經(jīng)發(fā)生且尚未結(jié)束的針對(duì)此文件的訪問(wèn)流程了。
[cpp] view plain copy
1. static void ____fput(struct callback_head *work)
2. {
3. __fput(container_of(work, struct file, f_u.fu_rcuhead));
4. }
5.
6.
7. void flush_delayed_fput(void)
8. {
9. delayed_fput(NULL);
10. }
11.
12. static DECLARE_WORK(delayed_fput_work, delayed_fput);
13.
14. void fput(struct file *file)
15. {
16. if (atomic_long_dec_and_test(&file->f_count)) {
17. struct task_struct *task = current;
18.
19. if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
20. init_task_work(&file->f_u.fu_rcuhead, ____fput);
21. if (!task_work_add(task, &file->f_u.fu_rcuhead, true))
22. return;
23. }
24.
25. if (llist_add(&file->f_u.fu_llist, &delayed_fput_list))
26. schedule_work(&delayed_fput_work);
27. }
28. }
現(xiàn)在再來(lái)想想,我們上面提到的那些訪問(wèn)file->private_data所指向的數(shù)據(jù)的異步流程,這些流程并沒(méi)有走文件系統(tǒng)框架。
會(huì)不會(huì)出現(xiàn)這種情況,xxx_release已經(jīng)執(zhí)行過(guò)了,可是異步流程卻還來(lái)訪問(wèn)file->private_data所指向的數(shù)據(jù)呢?
其實(shí)xxx_release不妨不要釋放file->private_data指向的內(nèi)存,而是標(biāo)記一下他的狀態(tài)為已關(guān)閉。然后異步流程再訪問(wèn)此數(shù)據(jù)時(shí),先檢查一下?tīng)顟B(tài)。
若為已關(guān)閉,則妥善處理并釋放即可。