date系统调用 linux_linux date系统调用 - CSDN
  • linux 时间系统 一 时间相关的系统调用 时间相关的系统调用,这里主要说明的是用来记录时间(打时间戳)和delay时间的系统调用。它们是linux时间系统的一部分。 时间相关的操作在应用层和内核层都很重要。下面的代码...

    linux 时间系统 一 时间相关的系统调用

    时间相关的系统调用,这里主要说明的是用来记录时间(打时间戳)和delay时间的系统调用。它们是linux时间系统的一部分。 时间相关的操作在应用层和内核层都很重要。下面的代码基于linux-4.9内核, ARCH=mips
    首先是两个比较重要的系统调用:
    gettimeofday

    #include <sys/time.h>
    int gettimeofday(struct timeval *tv, struct timezone *tz) 
    struct timeval {
                   time_t      tv_sec;     /* seconds */
                   suseconds_t tv_usec;    /* microseconds */
               };
    

    gettimeofday系统调用是用内核vsyscall实现的。查看进程的maps, vsyscall在vdso(virtual dynamic shared object)区域。vdso区域是进程启动时内核向进程映射一段空间,这样做是为了减少某些频繁调用的系统调用的开销。

    int __vdso_gettimeofday(struct timeval *tv, struct timezone *tz)
    {
    	const union mips_vdso_data *data = get_vdso_data();
    	struct timespec ts;
    	int ret;
    
    	ret = do_realtime(&ts, data);
    	if (ret)
    		return ret;
    
    	if (tv) {
    		tv->tv_sec = ts.tv_sec;
    		tv->tv_usec = ts.tv_nsec / 1000;
    	}
    
    	if (tz) {
    		tz->tz_minuteswest = data->tz_minuteswest;
    		tz->tz_dsttime = data->tz_dsttime;
    	}
    
    	return 0;
    }
    /* do realtime 直接读取导出的realtime时间*/
    static __always_inline int do_realtime(struct timespec *ts,
    				       const union mips_vdso_data *data)
    {
    	u32 start_seq;
    	u64 ns;
    
    	do {
    		start_seq = vdso_data_read_begin(data);
    
    		if (data->clock_mode == VDSO_CLOCK_NONE)
    			return -ENOSYS;
    
    		ts->tv_sec = data->xtime_sec;
    		ns = get_ns(data);
    	} while (vdso_data_read_retry(data, start_seq));
    
    	ts->tv_nsec = 0;
    	timespec_add_ns(ts, ns);
    
    	return 0;
    }
    
    
    

    下面是vdso导出的数据区域

    union mips_vdso_data {
    	struct {
    	/*timekeeper 维护的realtime时间*/
    		u64 xtime_sec;  
    		u64 xtime_nsec;
    	/*从realtime时间向monotonic时间的偏移*/
    		u32 wall_to_mono_sec;
    		u32 wall_to_mono_nsec;
    		u32 seq_count;
    		u32 cs_shift;
    		u8 clock_mode;
    		u32 cs_mult;
    		u64 cs_cycle_last;
    		u64 cs_mask;
    		s32 tz_minuteswest;
    		s32 tz_dsttime;
    	};
    
    	u8 page[PAGE_SIZE];
    };
    

    clock_gettime

    #include <time.h>
    int clock_gettime(clockid_t clk_id, struct timespec *tp);
    
     struct timespec {
                   time_t   tv_sec;        /* seconds */
                   long     tv_nsec;       /* nanoseconds */
               };
    

    clockid 用来指定选择哪一个clock。
    内核维护两个clock

    • CLOCK_REALTIME(wall clock): 墙上时间,记录从1970-01-01 00:00:00时刻开始的时间,当进行时间同步操作的时候会被修改。当没有进行时间同步时,跟CLOCK_MONOTONIC相同
    • CLOCK_MONOTONIC: 单调递增的时间,不受修改系统时间的影响。
    int __vdso_clock_gettime(clockid_t clkid, struct timespec *ts)
    {
    	const union mips_vdso_data *data = get_vdso_data();
    	int ret;
    
    	switch (clkid) {
    	case CLOCK_REALTIME_COARSE:
    		ret = do_realtime_coarse(ts, data);
    		break;
    	case CLOCK_MONOTONIC_COARSE:
    		ret = do_monotonic_coarse(ts, data);
    		break;
    	case CLOCK_REALTIME:
    		ret = do_realtime(ts, data);
    		break;
    	case CLOCK_MONOTONIC:
    		ret = do_monotonic(ts, data);
    		break;
    	default:
    		ret = -ENOSYS;
    		break;
    	}
    
    	/* If we return -ENOSYS libc should fall back to a syscall. */
    	return ret;
    }
    

    从代码中可以看出,当id是CLOCK_REALTIME时,进行的动作与gettimeofday相同。当id是CLOCK_MONOTONIC时,执行do_monotonic。

    static __always_inline int do_monotonic(struct timespec *ts,
    					const union mips_vdso_data *data)
    {
    	u32 start_seq;
    	u64 ns;
    	u32 to_mono_sec;
    	u32 to_mono_nsec;
    
    	do {
    		start_seq = vdso_data_read_begin(data);
    
    		if (data->clock_mode == VDSO_CLOCK_NONE)
    			return -ENOSYS;
    
    		ts->tv_sec = data->xtime_sec;
    		ns = get_ns(data);
    
    		to_mono_sec = data->wall_to_mono_sec;
    		to_mono_nsec = data->wall_to_mono_nsec;
    	} while (vdso_data_read_retry(data, start_seq));
    
    	ts->tv_sec += to_mono_sec;
    	ts->tv_nsec = 0;
    	timespec_add_ns(ts, ns + to_mono_nsec);
    
    	return 0;
    }
    

    do_monotonic同样是直接获取系统维护的时间xtime_sec, 但是后面要用wall_to_mono_*进行修正。
    下面的例子来说明两个的区别:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/time.h>
    #include <unistd.h>
    #include <time.h>
    
    const char *command = "date -s \"2018-10-24 09:00:00\"";
    int main(int argc, char *argv[])
    {
    	struct timeval time_now;
    	struct timespec time_test;
    	struct timeval time_change;
    
    	char buffer[64] = {0};
    
    	gettimeofday(&time_now, NULL);
    	strftime(buffer, 64, "Current date/time(1): %m-%d-%Y/%T", localtime(&time_now.tv_sec));
    	printf("%s\n", buffer);
    
    	printf("realtime:\n");
    	clock_gettime(CLOCK_REALTIME, &time_test);
    	time_change.tv_sec = time_test.tv_sec;
    	time_change.tv_usec = time_test.tv_nsec / 1000;
    	strftime(buffer, 64, "Current date/time(2): %m-%d-%Y/%T", localtime(&time_change.tv_sec));
    	printf("%s\n", buffer);
    
    	printf("monotonic time:\n");
    	clock_gettime(CLOCK_MONOTONIC, &time_test);
    	time_change.tv_sec = time_test.tv_sec;
    	time_change.tv_usec = time_test.tv_nsec / 1000;
    	strftime(buffer, 64, "Current date/time(3): %m-%d-%Y/%T", localtime(&time_change.tv_sec));
    	printf("%s\n", buffer);
    
    	system(command);
    
    	printf("\ndate change time %s:\n\n", command);
    
    	printf("time now:\n");
    	gettimeofday(&time_now, NULL);
    	strftime(buffer, 64, "Current date/time(1): %m-%d-%Y/%T", localtime(&time_now.tv_sec));
    	printf("%s\n", buffer);
    
    	printf("realtime:\n");
    	clock_gettime(CLOCK_REALTIME, &time_test);
    	time_change.tv_sec = time_test.tv_sec;
    	time_change.tv_usec = time_test.tv_nsec / 1000;
    	strftime(buffer, 64, "Current date/time(2): %m-%d-%Y/%T", localtime(&time_change.tv_sec));
    	printf("%s\n", buffer);
    
    	printf("monotonic time:\n");
    	clock_gettime(CLOCK_MONOTONIC, &time_test);
    	time_change.tv_sec = time_test.tv_sec;
    	time_change.tv_usec = time_test.tv_nsec / 1000;
    	strftime(buffer, 64, "Current date/time(3): %m-%d-%Y/%T", localtime(&time_change.tv_sec));
    	printf("%s\n", buffer);
    
    
    	return 0;
    }
    

    下边是系统刚启动一小段时间的运行结果:

    /mnt # ./date_test
    Current date/time(1): 01-01-1970/00:46:51
    realtime:
    Current date/time(2): 01-01-1970/00:46:51
    monotonic time:
    Current date/time(3): 01-01-1970/00:46:51
    Wed Oct 24 09:00:00 UTC 2018
    
    date change time date -s "2018-10-24 09:00:00":
    
    time now:
    Current date/time(1): 10-24-2018/09:00:00
    realtime:
    Current date/time(2): 10-24-2018/09:00:00
    monotonic time:
    Current date/time(3): 01-01-1970/00:46:51
    
    

    修改系统时间之后,用monotonic time 转化出来的localtime依然是从启动开始的实际间隔。

    展开全文
  • 很多人都在问Linux系统的write调用到底是不是原子的。网上能搜出一大堆文章,基本上要么是翻译一些文献,要么就是胡扯,本文中我来结合实例来试着做一个稍微好一点的回答。  先摆出结论吧。结论包含两点,即write...

    很多人都在问Linux系统的write调用到底是不是原子的。网上能搜出一大堆文章,基本上要么是翻译一些文献,要么就是胡扯,本文中我来结合实例来试着做一个稍微好一点的回答。


      先摆出结论吧。结论包含两点,即write调用不能保证什么以及write调用能保证什么

      首先,write调用不能保证你要求的调用是原子的,以下面的调用为例:

    ret = write(fd, buff, 512);

    Linux无法保证将512字节的buff写入文件这件事是原子的,因为:

    1. 即便你写了512字节那也只是最大512字节,buff不一定有512字节这么大;
    2. write操作有可能被信号中途打断,进而使得ret实际上小于512;
    3. 实现根据不同的系统而不同,且几乎都是分层,作为接口无法确保所有层资源预留。磁盘的缓冲区可能空间不足,导致底层操作失败。

    如果不考虑以上这些因素,write调用为什么不设计成直接返回True或者False呢?要么成功写入512字节,要么一点都不写入,这样岂不更好?之所以不这么设计,正是基于上述不可回避的因素来考虑的。

      在系统调用设计的意义上,不信任的价值大于信任,最坏的考虑优先于乐观地盲进

      其次,write调用能保证的是,不管它实际写入了多少数据,比如写入了n字节数据,在写入这n字节数据的时候,在所有共享文件描述符的线程或者进程之间,每一个write调用是原子的,不可打断的。举一个例子,比如线程1写入了3个字符’a’,线程2写入了3个字符’b’,结果一定是‘aaabbb’或者是‘bbbaaa’,不可能是类似‘abaabb’这类交错的情况。

      也许你自然而然会问一个问题,如果两个进程没有共享文件描述符呢?比如进程A和进程B分别独立地打开了一个文件,进程A写入3个字符’a’,进程B写入了3个字符’b’,结果怎样呢?

      答案是,这种情况下没有任何保证,最终的结果可能是‘aaabbb’或者是‘bbbaaa’,也可能是‘abaabb’这种交错的情况。如果你希望不交错,那么怎么办呢?答案也是有的,那就是在所有写进程打开文件的时候,采用O_APPEND方式打开即可。

      作为一个和用户态交互的典型系统调用,write无法保证用户要求的事情是原子的,但它在共享文件的范围内能保证它实际完成的事情是原子的,在非共享文件的情况下,虽然它甚至无法保证它完成的事情是原子的,但是却提供了一种机制可以做到这种保证。可见,write系统调用设计的非常之好,边界十分清晰!

      关于以上的这些保证是如何做到的,下面简要地解释下。我本来是不想解释的,但是看了下面的解释后,对于理解上述的保证很有帮助,所以就不得不追加了。解释归于下图所示:

    这里写图片描述

    总结一下套路:

    1. APPEND模式通过锁inode,保证每次写操作均在inode中获取的文件size后追加数据,写完后释放锁;
    2. 非APPEND模式通过锁file结构体后获取file结构体的pos字段,并将数据追加到pos后,写完更新pos字段后释放锁。

    由此可见,APPEND模式提供了文件层面的全局写安全,而非APPEND模式则提供了针对共享file结构体的进程/线程之间的写安全。

      值得一再重申的是,由于write调用只是在inode或者file层面上保证一次写操作的原子性,但无法保证用户需要写入的数据的一次肯定被写完,所以在多线程多进程文件共享情况下就需要用户态程序自己来应对short write问题,比如设计一个锁保护一个循环,直到写完成或者写出错,不然循环不退出(详见《UNIX网络编程》),锁不释放…

      此外,我们知道,apache,nginx以及另外一些服务器写日志都是通过APPEND来保证独立原子写入的,要知道这些日志对于这类服务器而言是极端重要的。


    本文写到这里貌似应该可以结束了吧。

      如果是这样,我是不会写这篇文章的,要不是发生了点什么事情,我绝不会写这种总结性的文章,这不是我的风格。既然写了这篇,说明下面才是重头戏!

      从一个悲哀的故事说起。

      我自己写了一个分析TCP数据包的程序,通过不断打日志的方式把数据包的信息记录在文件里,程序是个多线程程序,大概10多个线程同时写一个内存文件系统的文件,最后我发现少了一条日志!程序本身不是重点,我可以通过以下的小程序代之解释:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/prctl.h>
    #include <string.h>
    #include <unistd.h>
    
    char a[512];
    char b[16];
    
    int main()
    {
            int fd;
    
            memset(a, 'a', 512);
            memset(b, '-', 16);
    
            fd = open("/usr/src/probe/test.txt", O_RDWR|O_CREAT|O_TRUNC, 0660);
    
            if (fork() == 0) {
                    prctl(PR_SET_NAME, (unsigned long)"child");
                    write(fd, b, 16);
                    exit(0);
            }
            write(fd, a, 512);
            exit(0);
    }

    编译为parent并运行,你猜猜最后test.txt里面是什么内容?

      由于父子进程是共享fd指示的file结构体的,按照上面的解释,最终的文件内容肯定是下面两种中的一种:

    ----------------aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

    或者:

    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa----------------

    可是,事实并不是这样!事实上,在很小的概率下,文件中只有512个字符‘a’,没有看到任何字符‘-‘(当然还会有别的情况)!Why?

      你能理解,当事实和理论分析不符的时候是多么痛苦,标准上明明就是说要保证共享file结构体的进程/线程一次写操作的原子性,然而事实证明有部分内容确实是被覆盖了,这显然并不合理。

      再者说了,系统调用在设计之初就要做出某种级别的保证,比如一次操作的原子性等等,这样的系统API才更友好,我相信标准是对的,所以我就觉得这是代码的BUG所致。是这么个思路吗?

      不!上面的这段话是事后诸葛亮的言辞,本文其实是一篇倒叙,是我先发现了写操作被覆盖,进而去逐步排查,最终才找到本文最开始的那段理论的,而不是反过来。所以,在我看到这个莫名其妙的错误后,我并不知道这是否合理,我只是隐约记得我曾经写过的一篇文章:
    关于O_APPEND模式write的原子性http://blog.csdn.net/dog250/article/details/29185821
    这篇文章的写作背景我早就忘记了,我记得当时也是费了一番功夫,所以我只是依靠信仰觉得这次又是内核的BUG!然而我如何来证明呢?

      首先我要想到一个写操作被覆盖的场景,然后试着去重现这个场景,最终去修复它。首先第一步还是看代码,出问题的内核是3.10社区版内核,于是我找到源码:

    SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
            size_t, count)
    {
        struct fd f = fdget(fd);
        ssize_t ret = -EBADF;
    
        if (f.file) {
            loff_t pos = file_pos_read(f.file);
            ret = vfs_write(f.file, buf, count, &pos);
            file_pos_write(f.file, pos);
            fdput(f);
        }
    
        return ret;
    }

    说实话,这段代码我是分析了足足10分钟才发现一个race的,而且是参考了我之前的那篇文章。简单讲,我把这个系统调用分解为了三部分:

    1. get pos
    2. vfs_write
    3. update pos

    race发生在1和2或者2和3之间。以下图示之:

    这里写图片描述

    既然找到了就容易重现了,方法有两类,一类是拼命那个写啊写,碰运气重现,但这不是我的方式,另一种方法我比较喜欢,即故意放大race的条件!

      对于本文的场景,我使用jprobe机制故意在1和2之间插入了一个schedule。试着加载包含下面代码的模块:

    ssize_t jvfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
    {
            if (!strcmp(current->comm, "parent")) {
                    msleep(2000);
            }
    
            jprobe_return();
            return 0;
    }
    
    static struct jprobe delay_stub = {
            .kp = {
                    .symbol_name    = "vfs_write",
            },
            .entry  = jvfs_write,
    };

    我是HZ1000的机器,上述代码即在1和2之间睡眠2秒钟,这样几乎可以100%重现问题。

      试着跑了一遍,真的就重现了!文件中有512个字符‘a’,没有看到任何字符‘-‘

      看起来这问题在多CPU机器上是如此地容易重现,以至于任何人都会觉得这问题不可能会留到3.10内核还不被修补啊!但是内核源码摆在那里,确实是有问题啊!这个时候,我才想起去看一些文档,看看这到底是一个问题呢还是说这本身是合理的,只是需要用户态程序采用某种手段去规避(比如《UNIX环境高级编程》就特别爱用这种方式)。曲折之路就不多赘述了,直接man 2 write,看BUGS section

    BUGS
           According to POSIX.1-2008/SUSv4 Section XSI 2.9.7 ("Thread Interactions with Regular File Operations"):
    
               All of the following functions shall be atomic with respect to each other in the effects specified in POSIX.1-2008 when they operate on regular files or
               symbolic links: ...
    
           Among the APIs subsequently listed are write() and writev(2).  And among the effects that should be atomic across threads (and processes) are updates of the
           file offset.  However, on Linux before version 3.14, this was not the case: if two processes that share an open file description  (see  open(2))  perform  a
           write()  (or  writev(2)) at the same time, then the I/O operations were not atomic with respect updating the file offset, with the result that the blocks of
           data output by the two processes might (incorrectly) overlap.  This problem was fixed in Linux 3.14.
    

    嗯,说明3.10的内核真的是BUG,3.14以后的内核解决了,非常OK!看了4.14的内核,问题没有了,这问题早就在3.14社区内核中解决:

    SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
            size_t, count)
    {
        struct fd f = fdget_pos(fd);  // 这里会锁file的pos锁
        ssize_t ret = -EBADF;
    
        if (f.file) {
            loff_t pos = file_pos_read(f.file);
            ret = vfs_write(f.file, buf, count, &pos);
            if (ret >= 0)
                file_pos_write(f.file, pos);
            fdput_pos(f);
        }
    
        return ret;
    }

    针对该问题的patch说明在:https://lkml.org/lkml/2014/3/3/533

    From: Linus Torvalds <torvalds@linux-foundation.org>
    Date: Mon, 3 Mar 2014 09:36:58 -0800
    Subject: [PATCH 1/2] vfs: atomic f_pos accesses as per POSIX
    
    Our write() system call has always been atomic in the sense that you get
    the expected thread-safe contiguous write, but we haven't actually
    guaranteed that concurrent writes are serialized wrt f_pos accesses, so
    threads (or processes) that share a file descriptor and use "write()"
    concurrently would quite likely overwrite each others data.
    
    This violates POSIX.1-2008/SUSv4 Section XSI 2.9.7 that says:
    
     "2.9.7 Thread Interactions with Regular File Operations
    
      All of the following functions shall be atomic with respect to each
      other in the effects specified in POSIX.1-2008 when they operate on
      regular files or symbolic links: [...]"
    
    and one of the effects is the file position update.
    
    This unprotected file position behavior is not new behavior, and nobody
    has ever cared.  Until now.  Yongzhi Pan reported unexpected behavior to
    Michael Kerrisk that was due to this.
    
    This resolves the issue with a f_pos-specific lock that is taken by
    read/write/lseek on file descriptors that may be shared across threads
    or processes.

    一波三折的事情貌似结束了,总结一下收获就是,碰到问题直接看文档而不是代码估计可能会更快速解决问题。


    我禁不住把这份收获分享给了温州皮鞋老板和王姐姐,为了防止他们较真儿挑战,我准备整理一下我的环境,然后把重现方法也告诉他们,我重启了我的机器,问题发生了…


    这绝对是本文的最后一部分,如果再发生故事,我保证会放弃!因为这个问题本来就是碰到了顺便拿来玩玩的。

      当我把机器重启到Centos 2.6.32内核(我认为低版本内核更容易重现,更容易说明问题)时,依然载入我那个jprobe内核模块,运行我那个parent程序,然而并没有重现问题,相反地,当parent被那个msleep阻塞后,child同样也被阻塞了,看样子是修复bug后的行为啊。

      第一感觉这可能性不大,毕竟3.10内核都有的问题,2.6.32怎么可能避开?!然而事后仔细一想,不对,3.10的问题内核是社区内核,2.6.32的是Centos内核,后者会拉取很多的上游patch来解决一些显然的问题的,对于衍生自Redhat公司的稳定版内核,这并不稀奇。

      最后,我在以下的地址:
    https://oss.oracle.com/git/gitweb.cgi?p=redpatch.git;a=blob;f=fs/read_write.c;h=2e01b41be52b0a313a10fac1a6ebd7161901434a;hb=rhel-2.6.32-642.13.2.el6
    找到了write的实现:

    SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
                     size_t, count)
    {
            struct file *file;
            ssize_t ret = -EBADF;
            int fput_needed;
    
            file = fget_light_pos(fd, &fput_needed);  // 这里是关键
            if (file) {
                    loff_t pos = file_pos_read(file);
                    ret = vfs_write(file, buf, count, &pos);
                    file_pos_write(file, pos);
                    fput_light_pos(file, fput_needed);
            }
    
            return ret;
    }

    请注意fget_light_pos是一个新的实现:

    struct file *fget_light_pos(unsigned int fd, int *fput_needed)
    {
            struct file *file = fget_light(fd, fput_needed);
    
            if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
                    if (file_count(file) > 1) {
                            *fput_needed |= FDPUT_POS_UNLOCK;
                            // 如果有超过一个进程/线程在操作同一个file,则先lock它!
                            mutex_lock(&file->f_pos_lock);
                    }
            }
            return file;
    }

    事情就是在这里起了变化!Centos早就拉取了修复该问题的patch,解决了问题便无法重现问题。

      所以,社区版内核和发行版内核是完全不同的,侧重点不同吧,社区版内核可能更在意内核本身的子系统以及性能因素,而发行版内核则更看重稳定性以及系统调用,毕竟系统就是用来跑应用的,系统调用作为一个接口,一定要稳定无BUG!


    事情结束!

      以下是一点关于这次问题排查的补遗。

      这问题事后跟温州老板以及王姐姐讨论过,我关注的点在于,write一次写(不管实际上写了多少)到底能不能被打断,并不是写多少不会被打断,对于后者,说实话系统保证不了,比如说万一你要求写100T的数据,写着写着你Ctrl-C了或者机器断电了,你能咋滴,谁来负责?但是系统能保证的是,不管你写多少的数据,在你退出write调用前,都不可能被其它的写操作所打断。这是正确的系统调用行为,至于别的,系统并不保证!

      这就好比你去服务大厅排队办事,业务员完全有理由在受理你的业务期间由于你的疏忽或者她自己的疏忽让你仅仅办了一部分事或者说甚至无功而返,但决不会在正在受理你的业务同时又接待了别人,这样你就可以投诉她了吧。write调用的行为也是完全一模一样。

      有人说当文件类型是PIPE时,系统要求至少原子写入PIPE_BUF字节,对于普通文件,也差不多是这么多。这简直太牵强,之所以说是PIPE_BUF而不是硬写成是4096就是因为写操作的具体实现是系统实现相关的,取决于你拿什么作为载体作为到达磁盘的媒介,一般而言就是页面,一次申请的最小单位就是页面,因此刷入4096字节这么一个页面的大小的块是必须的。然而,如果一个页面是1字节呢?或者是1T呢?所以说,这并不是作为用户接口的系统调用所能承诺写入的理由。

      还是那句话,能写多少,系统决然无法保证(业务员无法阻止你忘记带身份证从而业务只能办理一半),但它能保证在它写的时候,不会被其它的写操作打断!


    标准规定的都是正确的,至少比代码更正确,先有的标准再有的代码。但这并不意味着实现就一定符合标准,实现是可以有bug的,比如Linux 3.14版本前社区版内核实现就有bug。所以写完本文后最大的收获就是先看标准和文档再看代码。其实,我是倾向于能不看代码就不看代码的,代码仅仅是一种实现方式而已,我认识的一些Cisco这种公司的网络技术大牛告诉过我,看手册,看Paper,看标准,跑测试case对于理解和玩转一个技术要比单纯的源码分析有效很多很多

      嗯,我就是那个送煤气罐的人。

    ————— 平安夜补遗 —————

    文档比代码重要吗?

    Linus说过“Talk is cheap. Show me the code”,但Document价值几何呢?Linus并没有说。

      我的意思是说,在排查问题的时候,首先要了解事情应该是什么样子的,而不是事情做成了什么样子coding的过程是充满乐趣的,但是一段有bug的code总是令人乏味的!以冒泡排序为例,如果你发现你的代码并没有完成一次正确的排序,首先你要确保你对冒泡排序真的已经理解了,其次才是去debug那段令人沮丧的代码。

      又扯到TCP了。我想很多人在学习Linux内核网络协议栈的时候都避开了TCP的实现,不光是我们这些凡人,就连基本讲Linux网络的经典的书都不包括TCP的内容。这是为什么呢?

      实话实说,Linux的TCP实现太复杂太庞大了,任何初学者看到Linux的TCP实现几乎都会望而却步,仅仅tcp_ack函数就够你喝一壶的了…我本人曾经看了大半年的这部分代码都没有搞明白TCP是怎么回事。后来我是怎么突然就懂了呢?

      我相信量变会引起质变,当我坚持死磕Linux TCP实现的代码,总有一天会看懂的,但是我觉得时间并不是决定性的因素。在我看了很久都没有看懂的时候,我其实不会死磕,我会放弃,我想很多人都会放弃。是的,我放弃了,我想如果给我时间,我能写出一个TCP实现,并且这将是一个我肯定能看懂的TCP实现。

      转而我开始看TCP相关的各种RFC标准,后面我就不说了,反正结果就是看了半个月RFC(上班路上看,上班看,下班路上看,晚上看,周末看,梦里还看…),然后再看Linux内核TCP实现的代码,就秒懂了,这是真事儿。闲着没事儿的时候,还自己试着写了一个用户态的TCP实现版本,用Tap虚拟网卡来通信,后来发现这个跟uIP,lwIP这些有点重复,就没有继续下去…既然知道了事情应该做成什么样子,那么如何去做就不重要了。

      如果程序符合预期,你很好奇这是怎么做到的,那么代码里面没有秘密。
      如果程序不符合预期,第一要务是搞清楚正确的做法,而不是直接去看有bug的代码。

    关于write原子性的讨论

    昨天把这篇文章发到了朋友圈,有位朋友马上就回复了,以下是回复内容,作为本文的补充:

    A:原子写一般只保证512字节,一个sector的大小。
    B:这个确实是实现相关的,现在很多实现基于Page cache,就成了一个Page的大小。按照物理实现,当前的SSD可能又有不同。。。
    A:固件不支持,内核再怎么变也没用。所以才有mysql的double write buffer,就是因为hdd原子写是一个扇区。

    很不错的视角。

      这里要补充的是,一个写操作从发起到磁盘,经过了太多的层,其中每一个层都有该层的最小操作单位,在VFS到磁盘缓冲的层,Page就是最小单位,再往下到磁盘的时候,扇区就成了最小单位,也许某种新的磁盘操作的并不是扇区,也许最终的文件就是个内存块,也可能是NFS的网络对端…总之,内核是无法对应用程序做出原子保证的,很简单,在系统调用这个层次,太高了,系统在这里对底层并不知情,当然无法获知底层所做出的任何承诺。

      对于某些实现,可以通过ioctl这类带外控制通道的调用获取底层的元数据,这样至少可以让应用程序可以对自己的行为行使更多的策略,拥有更多的选择。

    平安夜礼物

    2002年平安夜,我和几个屌丝在哈尔滨中央大街看橱窗里穿着白色礼服的俄罗斯美女,不用买礼物,带着眼就行,足足在一家婚纱店外面抽了5根烟才离去。
      后面几年跟这个差不多,只不过换了地方换了几个不同的屌丝而已。我记得2004年圣诞节前我是从河南工学院一路滑着冰到郑州火车站的,然后逃票到了新乡去找女朋友(现在成了小小的妈)。
      2007年平安夜,吉林长春,欧亚商都,巴黎春天。我本来是想给女朋友买件大衣的,然而价格均在800块以上,沮丧地离开商场在门外买了一串气球回家,买不起衣服,吃不起东方肉馆…
      后来到了上海,终于能买得起了,女朋友也成了老婆,然而也胖了不再买衣服了,也不再把肉食作为奢侈品了,这是多么的幸福,没钱的时候,买不起,有钱的时候,不用买了。
      然而事情在起变化。
      躲了老婆,躲不了情人。我想送小小一件圣诞节礼物,是的,我想了很久了。然而不知道怎么回事,平安夜就在眼前了…中午的时候,我出去晃悠了一下,想看看能不能找点灵感,然而还是令人痛苦地失败了。我买了一个超大的健达蛋回家,突然感受到了2007年平安夜买那串气球的感觉。
      刚进门,就被小小堵截了,我被迫把礼物给了她,她非常之高兴,我感到很惭愧,一个23块钱的礼物在她眼里有如此昂贵。唉,温州老板说我空手套白狼,我想温州老板是对的。都说女儿是老爸上辈子的情人,上辈子没能给她幸福,这辈子呢?作为父亲的我和女儿其实没有任何关系,一切都是因为缘分,不管是良缘还是孽缘,让她选择了做我的女儿,所以一定要对她好,毕竟她本来可能是有选择父亲的权利的。
      圣诞节是基督教的节日,我也是基督教的信徒,我本该多说点,但还是选择保持缄默,闭上眼睛,心里祷告。眼里有工作,心中有上帝,四海共见美好。

      愿圣诞节的清晨,使我们做为您的孩子,幸福快乐,愿圣诞节的夜晚引领我们来到床前,用感恩的心为你述说,赦免我的过去,赦免我的现在,因着耶稣基督的缘故,赦免我,这样的祷告,是奉我主耶稣基督的名!阿门!

    ———- 2017/12/24 22:13 作文———-

    展开全文
  • 后续的整个过程都是透明的,为了了解文件系统在其中起到了什么作用,又是如何和内核的其他部分进行协作的,我们可以对Read()函数进行追踪,下面的代码均来自linux2.6.11.10版本的内核。 每一串代码前我都标好了路径...

    当我们在C程序中用到某些库函数进行文件读取操作时,后续的整个过程都是透明的,为了了解文件系统在其中起到了什么作用,又是如何和内核的其他部分进行协作的,我们可以对Read()函数进行追踪,下面的代码均来自linux2.6.11.10版本的内核。

    每一串代码前我都标好了路径。

    首先,我们写下如下的测试程序,test.c,其中1.txt里只有一句Hello,World。

    #include <stdio.h>
    #include <stdlib.h>
    int main() {
        char word[20];
        FILE *fp;
        if((fp = fopen("1.txt","a+")) == NULL) {
            fprintf(stdout, "ERROR!");  
            exit(EXIT_FAILURE);
        }
        fscanf(fp,"%s",word);
        printf("%s\n",word);                             
        return 0;                                                
    }
    

    然后进行编译,并通过strace 工具查看函数运行时用到了哪些系统调用函数,并将结果输出到hello.txt中。

    ~/test$ gcc hello.c -o hello
    
    ~/test$ strace -o hello.txt ./hello
    

    查看hello.txt中的主要内容如下

    ……
    openat(AT_FDCWD, "x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT
    ……
    openat(AT_FDCWD, "1.txt", O_RDWR|O_CREAT|O_APPEND, 0666) = 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=13, ...}) = 0
    read(3, "Hello,World!\n", 4096)         = 13
    fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
    write(1, "Hello,World!\n", 13)          = 13
    lseek(3, -1, SEEK_CUR)                  = 12  
    exit_group(0)                           = ?
    

    可以看到首先打开了libc.so,这里面封装了我们需要的库函数,而后调用了write、read、lseek等库函数。

    我们知道,系统调用有两种方式实现,一种是老旧的int $0x80方式,还有一种是sysenter,具体细节不纠结,但过程总是先将系统调用号存入$eax,然后进行系统调用,这部分实现已经完全放进库函数了,进行系统调用后,会查系统调用表,比如read的系统调用就是3,那么查表就能查到这个函数。

    比如i386处理器的系统调用号局部如下所示

    /linux-2.6.11.10/include/asm-i386/unistd.h
    #define __NR_restart_syscall      0
    #define __NR_exit         1
    #define __NR_fork         2
    #define __NR_read         3
    #define __NR_write        4
    #define __NR_open         5
    #define __NR_close        6
    #define __NR_waitpid      7
    #define __NR_creat        8
    #define __NR_link         9                          
    #define __NR_unlink      10                           
    #define __NR_execve      11                            
    #define __NR_chdir       12                           
    #define __NR_time        13                            
    #define __NR_mknod       14                           
    #define __NR_chmod       15                           
    #define __NR_lchown      16                            
    #define __NR_break       17
    

    由上我们看到,调用read的系统调用号为3,在这个文件的下面我们还能看到比较老旧的系统调用实现代码,现在这个功能好像已经放到库中去实现了,不在内核中实现,这里内核版本较老,所以在内核中还能看到,这里用的是通过系统调用需要的参数个数来进行区分的。

    /linux-2.6.11.10/include/asm-i386/unistd.h
    #define __syscall_return(type, res) \                
    do { \
        if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \
            errno = -(res); \
            res = -1; \
        } \
        return (type) (res); \
    } while (0)
    /* XXX - _foo needs to be __foo, while __NR_bar could be _NR_bar. */
    #define _syscall0(type,name) \
    type name(void) \
    { \
    long __res; \
    __asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name)); \  
    __syscall_return(type,__res); \                                                            
    }
    

    而之后,read会调用相应的服务例程sys_read,此函数定义如下。

    /linux-2.6.11.10/fs/read_write.c
    asmlinkage ssize_t sys_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); //buf为用户态缓冲区,count为读取长度
            file_pos_write(file, pos);  //将新的偏移地址写回文件
            fput_light(file, fput_needed); //释放文件
        }
    
        return ret;
    }
    EXPORT_SYMBOL_GPL(sys_read);
    

    该函数首先通过fget_light(light表示轻量级的)通过文件描述符,来返回一个文件地址,类型为虚拟文件系统层的struct file,然后获取文件偏移地址,并调用vfs_read即虚拟文件系统的读操作,从这里我们可以看到,无论底层是什么文件系统,由于有VFS这个中间层存在,对文件进行操作都可以把事情交给VFS来处理,这是抽象的好处。

    我们可以在sys_read所在的文件里找到vfs_read。

    /linux-2.6.11.10/fs/read_write.c
    ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
    {
        ssize_t ret;
    
        if (!(file->f_mode & FMODE_READ))  //进程的访问模式是否可读文件
            return -EBADF;
        if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read)) //检查文件是否定义有相关操作
            return -EINVAL;
        if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) //粗略检查参数,看缓冲区是否有效
            return -EFAULT;
    
        ret = rw_verify_area(READ, file, pos, count);  //检查当前区域是否有锁
        if (!ret) {
            ret = security_file_permission (file, MAY_READ);  //检查是否有读的权限
            if (!ret) {
                if (file->f_op->read)
                    ret = file->f_op->read(file, buf, count, pos);   //如有则调用相应文件系统的read函数
                else
                    ret = do_sync_read(file, buf, count, pos);   //否则调用这个函数
                if (ret > 0) {
                    dnotify_parent(file->f_dentry, DN_ACCESS);   //通知父目录文件已获取
                    current->rchar += ret;
                }
                current->syscr++;   //一些I/O次数的统计
            }
        }
        return ret;
    }
    
    EXPORT_SYMBOL(vfs_read);
    

    我们可以看到,vfs_read函数只是检查了一些状态,就使用回调函数 file->f_op->read,使用相应文件系统的read函数继续进行操作,这个file_operations应该是open file的时候就已经填好的,我们可以/linux-2.6.11.10/fs/ext2/file.c里找到ext2所有的文件操作,如下,其实在新内核里,read和write之类的操作已经改了。

    /linux-2.6.11.10/fs/ext2/file.c
    struct file_operations ext2_file_operations = {
        .llseek        = generic_file_llseek,
        .read        = generic_file_read,
        .write        = generic_file_write,
        .aio_read    = generic_file_aio_read,
        .aio_write    = generic_file_aio_write,
        .ioctl        = ext2_ioctl,
        .mmap        = generic_file_mmap,
        .open        = generic_file_open,
        .release    = ext2_release_file,
        .fsync        = ext2_sync_file,
        .readv        = generic_file_readv,
        .writev        = generic_file_writev,
        .sendfile    = generic_file_sendfile,
    };
    

    可以看到,ext2的read操作并没有额外定义,而是使用了一个通用文件读函数,在/linux-2.6.11.10/mm/filemap.c文件里可以找到这个函数,因为读写是基于页操作的。

    /linux-2.6.11.10/mm/filemap.c
    ssize_t generic_file_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
    {
        struct iovec local_iov = { .iov_base = buf, .iov_len = count }; //用local_iov存用户缓区和读取长度
        struct kiocb kiocb; //同步和异步I/O操作描述符
        ssize_t ret;
    
        init_sync_kiocb(&kiocb, filp); //初始化描述符
        ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos); //所有文件系统使用的通用例程
        if (-EIOCBQUEUED == ret)  //如果在排队
            ret = wait_on_sync_kiocb(&kiocb);
        return ret;
    }
    EXPORT_SYMBOL(generic_file_read);
    

    这个函数继续调用了一个通用例程,即__generic_file_aio_read,字面理解就是异步I/O读,它不是立即读取,而是会先在一个链表里排队,如果在排队就需要继续等。

    /linux-2.6.11.10/mm/filemap.c
    ssize_t
    __generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov,
            unsigned long nr_segs, loff_t *ppos)
    {
        struct file *filp = iocb->ki_filp;  //与正在进行的read操作相关的文件对象指针
        ssize_t retval;
        unsigned long seg;  
        size_t count;
    
        count = 0;
        for (seg = 0; seg < nr_segs; seg++) {
            const struct iovec *iv = &iov[seg];
    
            /*
             * If any segment has a negative length, or the cumulative
             * length ever wraps negative then return -EINVAL.
             */
            count += iv->iov_len;
            if (unlikely((ssize_t)(count|iv->iov_len) < 0))
                return -EINVAL;
            if (access_ok(VERIFY_WRITE, iv->iov_base, iv->iov_len))   //检查ivoec描述符所描述的用户态缓冲区是否有效
                continue;
            if (seg == 0)
                return -EFAULT;
            nr_segs = seg;
            count -= iv->iov_len;    /* This segment is no good */
            break;
        }
        /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */
        if (filp->f_flags & O_DIRECT) {    //直接I/O模式
            loff_t pos = *ppos, size;
            struct address_space *mapping;
            struct inode *inode;
    
            mapping = filp->f_mapping;
            inode = mapping->host;
            retval = 0;
            if (!count)
                goto out; /* skip atime */
            size = i_size_read(inode);
            if (pos < size) {
                retval = generic_file_direct_IO(READ, iocb,
                            iov, pos, nr_segs);
                if (retval >= 0 && !is_sync_kiocb(iocb))
                    retval = -EIOCBQUEUED;
                if (retval > 0)
                    *ppos = pos + retval;
            }
            file_accessed(filp);
            goto out;
        }
        retval = 0;    //如果不是直接I/O模式的话,就用页高速缓存
        if (count) {
            for (seg = 0; seg < nr_segs; seg++) {
                read_descriptor_t desc;  //定义读操作描述符
    
                desc.written = 0;
                desc.arg.buf = iov[seg].iov_base;  //用户缓冲区
                desc.count = iov[seg].iov_len;   //读取长度
                if (desc.count == 0)
                    continue;
                desc.error = 0;
                do_generic_file_read(filp,ppos,&desc,file_read_actor); //调用该函数读文件
                retval += desc.written;
                if (!retval) {
                    retval = desc.error;
                    break;
                }
            }
        }
    out:
        return retval;
    }
    EXPORT_SYMBOL(__generic_file_aio_read);
    

    我们可以将上面这个函数粗略划分为三部分,检查部分,以及直接I/O读取,和页高速缓存读取,如果设置了O_DIRECT标志,则直接读取调用generic_file_direct_IO(),否则要使用页高速缓存,调用do_generic_file_read,,我们主要关注页高速缓存读取。

    /linux-2.6.11.10/include/linux/fs.h
    static inline void do_generic_file_read(struct file * filp, loff_t *ppos,
                        read_descriptor_t * desc,
                        read_actor_t actor)
    {
        do_generic_mapping_read(filp->f_mapping,
                    &filp->f_ra,
                    filp,
                    ppos,
                    desc,
                    actor);
    }
    

    do_generic_file_read会继续调用do_generic_mapping_read,这个调用表示对文件的读操作转换为对页高速缓存的读操作。

    之所以要在I/O过程中加入页高速缓存这么一个缓冲层,是为了提高读取的效率,我们希望能尽量减少对磁盘的读取,而将读取放到内存中进行,所以引入页高速缓存这么一个中间层。

    上面的参数中有一个filp->f_mapping,这个是一个地址空间变量,其定义如下。

    /linux-2.6.11.10/include/linux/fs.h
    struct address_space {
        struct inode        *host;        /* owner: inode, block_device */
        struct radix_tree_root    page_tree;    /* radix tree of all pages */
        spinlock_t        tree_lock;    /* and spinlock protecting it */
        unsigned int        i_mmap_writable;/* count VM_SHARED mappings */
        struct prio_tree_root    i_mmap;        /* tree of private and shared mappings */
        struct list_head    i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
        spinlock_t        i_mmap_lock;    /* protect tree, count, list */
        unsigned int        truncate_count;    /* Cover race condition with truncate */
        unsigned long        nrpages;    /* number of total pages */
        pgoff_t            writeback_index;/* writeback starts here */
        struct address_space_operations *a_ops;    /* methods */
        unsigned long        flags;        /* error bits/gfp mask */
        struct backing_dev_info *backing_dev_info; /* device readahead, etc */
        spinlock_t        private_lock;    /* for use by the address_space */
        struct list_head    private_list;    /* ditto */
        struct address_space    *assoc_mapping;    /* ditto */
    } __attribute__((aligned(sizeof(long))));
    

    通过host和page_tree两个属性,一个adrees_space结构体可以将一个文件和属于它的缓存页联系起来,page_tree是struct radix_tree_root类型的,就是一颗树的根,它指向一颗基树,相应的页都存在叶子节点上。

    在do_generic_mapping_read里,检查完基础数据后,会建立一个循环,这个循环每次读一页内容,直到读完所有内容。

    首先是find_page,它会通过关联有页的基树找到相应的页,如果没找到,就跳到no_cached_page重新分配一个页插入到基树里去,如果为脏页则需要更新,如果既能找到,又不需要更新,那么直接page_ok将数据拷贝到用户态即可。

    /linux-2.6.11.10/mm/filemap.c——do_generic_mapping_read
    find_page:
            page = find_get_page(mapping, index);  //首先在页高速缓存里寻找页描述符
            if (unlikely(page == NULL)) {
                handle_ra_miss(mapping, &ra, index);
                goto no_cached_page;
            }
            if (!PageUptodate(page)) //检查是否为脏页
                goto page_not_up_to_date;
    

    找到页以后开始读页,主要的重点语句是这一句

    linux-2.6.11.10/mm/filemap.c——do_generic_mapping_read
    readpage:
            /* Start the actual read. The read will unlock the page. */
            error = mapping->a_ops->readpage(filp, page);
            if (unlikely(error))
                goto readpage_error;
            if (!PageUptodate(page)) {
                lock_page(page);
                if (!PageUptodate(page)) {
                    if (page->mapping == NULL) {
                        /*
                         * invalidate_inode_pages got it
                         */
                        unlock_page(page);
                        page_cache_release(page);
                        goto find_page;
                    }
                    unlock_page(page);
                    error = -EIO;
                    goto readpage_error;
                }
                unlock_page(page);
            }
            /*
             * i_size must be checked after we have done ->readpage.
             *
             * Checking i_size after the readpage allows us to calculate
             * the correct value for "nr", which means the zero-filled
             * part of the page is not copied back to userspace (unless
             * another truncate extends the file - this is desired though).
             */
            isize = i_size_read(inode);
            end_index = (isize - 1) >> PAGE_CACHE_SHIFT;
            if (unlikely(!isize || index > end_index)) {
                page_cache_release(page);
                goto out;
            }
            /* nr is the maximum number of bytes to copy from this page */
            nr = PAGE_CACHE_SIZE;
            if (index == end_index) {
                nr = ((isize - 1) & ~PAGE_CACHE_MASK) + 1;
                if (nr <= offset) {
                    page_cache_release(page);
                    goto out;
                }
            }
            nr = nr - offset;
            goto page_ok;
    

    这里又出现了一个回调函数,又调用了相关文件系统的相关函数,我们可以找到ext2的a_ops定义如下:

    /linux-2.6.11.10/fs/ext2/inode.c
    struct address_space_operations ext2_aops = {
        .readpage        = ext2_readpage,
        .readpages        = ext2_readpages,
        .writepage        = ext2_writepage,
        .sync_page        = block_sync_page,
        .prepare_write        = ext2_prepare_write,
        .commit_write        = generic_commit_write,
        .bmap            = ext2_bmap,
        .direct_IO        = ext2_direct_IO,
        .writepages        = ext2_writepages,
    };
    

    再找到ext2_readpage开始我们的读页操作。

    /linux-2.6.11.10/fs/ext2/inode.c
    static int ext2_readpage(struct file *file, struct page *page)
    {
        return mpage_readpage(page, ext2_get_block);
    }
    

    这里它又继续调用了一个通用例程mapge_readpage,导入了页地址以及ext2的数据块寻址函数。

    /linux-2.6.11.10/fs/mpage.c
    int mpage_readpage(struct page *page, get_block_t get_block)
    {
        struct bio *bio = NULL;
        sector_t last_block_in_bio = 0;
    
        bio = do_mpage_readpage(bio, page, 1,
                &last_block_in_bio, get_block);
        if (bio)
            mpage_bio_submit(READ, bio);
        return 0;
    }
    EXPORT_SYMBOL(mpage_readpage);
    

    这里就两步操作,申请一个struct bio对象,然后提交这个任务。bio是通用块层用来管理传输数据的,他把一个磁盘存储区和一块内存区域联系起来。

    然后提交这个任务,这里面其实还有一个调度过程,所有的bio请求都在一个队列里,它可以重排读写数据块的请求,在重复访问文件同一个部分或多进程访问同一数据,可以大大提高读取效率。

    最终,这件读操作会交给磁盘的设备驱动程序来进行真正的数据操作。

    读完以后,再回到do_generic_mapping_read,跳到page_ok,它会调用__copy_to_user()函数将数据拷贝到用户态缓冲区,

    linux-2.6.11.10/mm/filemap.c——do_generic_mapping_read
    page_ok:
            /* If users can be writing to this page using arbitrary
             * virtual addresses, take care about potential aliasing
             * before reading the page on the kernel side.
             */
            if (mapping_writably_mapped(mapping))
                flush_dcache_page(page);
            /*
             * When (part of) the same page is read multiple times
             * in succession, only mark it as accessed the first time.
             */
            if (prev_index != index)
                mark_page_accessed(page);
            prev_index = index;
            /*
             * Ok, we have the page, and it's up-to-date, so
             * now we can copy it to user space...
             *
             * The actor routine returns how many bytes were actually used..
             * NOTE! This may not be the same as how much of a user buffer
             * we filled up (we may be padding etc), so we can only update
             * "pos" here (the actor routine has to update the user buffer
             * pointers and the remaining count).
             */
            ret = actor(desc, page, offset, nr);
            offset += ret;
            index += offset >> PAGE_CACHE_SHIFT;
            offset &= ~PAGE_CACHE_MASK;
            page_cache_release(page);
            if (ret == nr && desc->count)
                continue;
            goto out;
    
    linux-2.6.11.10/mm/filemap.c
    int file_read_actor(read_descriptor_t *desc, struct page *page,
                unsigned long offset, unsigned long size)
    {
        char *kaddr;
        unsigned long left, count = desc->count;
    
        if (size > count)
            size = count;
    
        /*
         * Faults on the destination of a read are common, so do it before
         * taking the kmap.
         */
        if (!fault_in_pages_writeable(desc->arg.buf, size)) {
            kaddr = kmap_atomic(page, KM_USER0);
            left = __copy_to_user_inatomic(desc->arg.buf,
                            kaddr + offset, size);
            kunmap_atomic(kaddr, KM_USER0);
            if (left == 0)
                goto success;
        }
    
        /* Do it the slow way */
        kaddr = kmap(page);
        left = __copy_to_user(desc->arg.buf, kaddr + offset, size);
        kunmap(page);
    
        if (left) {
            size -= left;
            desc->error = -EFAULT;
        }
    success:
        desc->count = count - size;
        desc->written += size;
        desc->arg.buf += size;
        return size;
    }
    

    然后更新一些计数,再一步步往上返回到最开始的read()系统调用,调用就结束了。

    展开全文
  • 在写shell脚本时经常用到date这个函数!很简单也很方便,确容易出错 例如:格式化出来一个日期 做日志文件名.然后再过几天把它删掉这样都很方便的维护磁盘空间 今天确发现了一个奇怪的地方 以下是执行命令: date 2017年...

    在写shell脚本时经常用到date这个函数!很简单也很方便,确容易出错

    例如:格式化出来一个日期 做日志文件名.然后再过几天把它删掉这样都很方便的维护磁盘空间

    今天确发现了一个奇怪的地方

    以下是执行命令:

    date
    2017年 07月 11日 星期二 09:47:15 CST


    date -d " day" "+%Y%m%d"

    20170712(本意要获取当天的时间也就是11号,实际运行确获取了后一天的时间)



    date -d "-0 day" "+%Y%m%d"

    20170711


    date  +%Y%m%d

    20170711(在样才是当天的时间)




    展开全文
  • 一、实验目的 1)理解操作系统生成的概念和...2)在Unbantu或Fedora环境下为Linux内核增加1-3 个新的系统调用,并启用新的内核,编写一个应用程序测试新增加的系统调用是否能正确工作。 3)在windows 环境下,利...
  • $ date "+%Y-%m-%d %H:%M:%S" 参考资料: 1、linux在shell中获取时间 ... 2、Linux系统date命令的参数及获取时间戳的方法 https://www.cnblogs.com/33debug/p/6632172.html ...
  • Linux系统调用

    2013-11-17 01:12:09
    Linux系统调用接口、系统调用例程和内核服务例程之间的关系 向linux内核中添加三个系统调用(Ubuntu9.10) ++++++++++++++++++++++++++++++++++ 使用 Linux 系统调用的内核命令 作者:M. Tim Jones 转贴自:本...
  • 先小说两句:今天研究了下PHP调用LINUX命令的功能,一开始怎么做都调用不成功,试了好久才终于成功了,所以发出来分享一下。下面我将详细介绍:  PHP中提供了几个调用linux命令的函数,exec、system、passthru,...
  • 系统调用 就是用户空间应用程序和内核提供的服务之间的一个接口。由于服务是在内核中提供的...在本文中,我将探究 Linux SCI,演示如何向 2.6.20 内核添加一个系统调用,然后从用户空间来使用这个函数。我们还将研究在
  • 很多时候需要各种格式的时间,因此date就到了大显身手的时候。 1、默认格式 [root@CentOS-7-2 ~]# date Tue Aug 7 16:24:09 CST 2018 [root@CentOS-7-2 ~]# 2、年月日时分秒 [root@CentOS-7-2 ~]# date +&...
  • 有时候经常会碰到需要远程调用Linux或者本地调用Linux或者本地调用cmd的一些命令,最近小结了一下这几种用法本地调用cmd命令 @Test public void testCmd()throws Exception{ String cmd="cmd /c date"; //命令的...
  • #传参调用exe程序(解决相对路径,觉得路径问题),等待exe进程结束,此程序才结束。 # -*- coding: utf-8-*- import os, os.path, sys import win32process, win32event exe_path = sys.argv[1] exe_file = sys....
  • Shell 调用系统时间变量  获取今天时期:`date +%Y%m%d` 或 `date +%F` 或 $(date +%y%m%d)  获取昨天时期:`date -d yesterday +%Y%m%d`  获取前天日期:`date -d -2day +%Y%m%d`  依次类推比如获取10天...
  • 详细描述:GPS授时程序中,GPS信号到并解析完毕后,调用system(set_date_string)进行授时,测得system过程耗时1ms左右,然而我们的系统要求时间精度必须在10us级别。因此请教一下各位大神,有没有**更快的方案**实现...
  • 本文将探究 Linux 系统调用接口(SCI),学习如何添加新的系统调用(以及实现这种功能的其他方法),并介绍与 SCI 有关的一些工具。 系统调用就是用户空间应用程序和内核提供的服务之间的一个接口。由于服务是在...
  • perl中调用Linux命令

    2009-07-13 17:00:00
    1. system(”command”);使用该命令将开启一个子进程执行引号中的命令,父进程将等待子进程结束并继续执行下面的代码。...使用反引号调用外部命令能够捕获其标准输出,并按行返回且每行结束处附带一个
  • 实验目标:尝试在nachos中运行用户程序,并熟悉将要完成的系统调用。 在完成实验后应该有如下了解 1用户程序是如何启动的 2 用户程序是如何通过系统调用与os内核交互的 3系统调用是如何实现的准备工作
  • 很多时候我们希望可以看到一个进程调用了哪些API以及其调用顺序,例如我们要参考某个程序的实现,但我们又无法获得该程序的源...strace和dtruss都是同一类型的命令,strace是linux系统上的,而dtruss是mac系统上的。
  • Linux命令——echo、date

    2018-07-12 23:26:56
    1.echo命令echo命令用于在终端输出字符串或变量提取后的值,格式为“echo [字符串 | $变量]”。例如,把指定字符串“Linuxprobe.com”输出到终端屏幕的命令为: [root@linuxprobe ~]# echo ...并将其输出到屏...
  • Linux 系统调用再探

    2009-01-30 18:16:00
    关于系统调用的实现机制以及在内核级加入新的系统调用的文章已经比较多了,在我们的《Linux操作系统原理与应用》一书中,给出了添加系统调用的两种方式,其中有一个实例通过系统调用对内核的运行过程进行...
1 2 3 4 5 ... 20
收藏数 81,534
精华内容 32,613
热门标签
关键字:

date系统调用 linux