1. 程式人生 > >再探Linux核心write系統呼叫操作的原子性

再探Linux核心write系統呼叫操作的原子性

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

                       

很多人都在問Linux系統的write呼叫到底是不是原子的。網上能搜出一大堆文章,基本上要麼是翻譯一些文獻,要麼就是胡扯,本文中我來結合例項來試著做一個稍微好一點的回答。


  先擺出結論吧。結論包含兩點,即write呼叫不能保證什麼以及write呼叫能保證什麼

  首先,write呼叫不能保證你要求的呼叫是原子的,以下面的呼叫為例:

ret = write(fd, buff, 512);
   
  • 1

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);}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

編譯為parent並執行,你猜猜最後test.txt裡面是什麼內容?

  由於父子程序是共享fd指示的file結構體的,按照上面的解釋,最終的檔案內容肯定是下面兩種中的一種:

----------------aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
   
  • 1

或者:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa----------------
   
  • 1

可是,事實並不是這樣!事實上,在很小的概率下,檔案中只有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;}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

說實話,這段程式碼我是分析了足足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,};
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我是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.
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

嗯,說明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;}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

針對該問題的patch說明在:https://lkml.org/lkml/2014/3/3/533

From: Linus Torvalds <[email protected]>Date: Mon, 3 Mar 2014 09:36:58 -0800Subject: [PATCH 1/2] vfs: atomic f_pos accesses as per POSIXOur write() system call has always been atomic in the sense that you getthe expected thread-safe contiguous write, but we haven't actuallyguaranteed that concurrent writes are serialized wrt f_pos accesses, sothreads (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 nobodyhas ever cared.  Until now.  Yongzhi Pan reported unexpected behavior toMichael Kerrisk that was due to this.This resolves the issue with a f_pos-specific lock that is taken byread/write/lseek on file descriptors that may be shared across threadsor processes.
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

一波三折的事情貌似結束了,總結一下收穫就是,碰到問題直接看文件而不是程式碼估計可能會更快速解決問題。


我禁不住把這份收穫分享給了溫州皮鞋老闆和王姐姐,為了防止他們較真兒挑戰,我準備整理一下我的環境,然後把重現方法也告訴他們,我重啟了我的機器,問題發生了…


這絕對是本文的最後一部分,如果再發生故事,我保證會放棄!因為這個問題本來就是碰到了順便拿來玩玩的。

  當我把機器重啟到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;}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

請注意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;}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

事情就是在這裡起了變化!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原子寫是一個扇區。
   
  • 1
  • 2
  • 3

很不錯的視角。

  這裡要補充的是,一個寫操作從發起到磁碟,經過了太多的層,其中每一個層都有該層的最小操作單位,在VFS到磁碟緩衝的層,Page就是最小單位,再往下到磁碟的時候,扇區就成了最小單位,也許某種新的磁碟操作的並不是扇區,也許最終的檔案就是個記憶體塊,也可能是NFS的網路對端…總之,核心是無法對應用程式做出原子保證的,很簡單,在系統呼叫這個層次,太高了,系統在這裡對底層並不知情,當然無法獲知底層所做出的任何承諾。

  對於某些實現,可以通過ioctl這類帶外控制通道的呼叫獲取底層的元資料,這樣至少可以讓應用程式可以對自己的行為行使更多的策略,擁有更多的選擇。

平安夜禮物

2002年平安夜,我和幾個屌絲在哈爾濱中央大街看櫥窗裡穿著白色禮服的俄羅斯美女,不用買禮物,帶著眼就行,足足在一家婚紗店外面抽了5根菸才離去。
  後面幾年跟這個差不多,只不過換了地方換了幾個不同的屌絲而已。我記得2004年聖誕節前我是從河南工學院一路滑著冰到鄭州火車站的,然後逃票到了新鄉去找女朋友(現在成了小小的媽)。
  2007年平安夜,吉林長春,歐亞商都,巴黎春天。我本來是想給女朋友買件大衣的,然而價格均在800塊以上,沮喪地離開商場在門外買了一串氣球回家,買不起衣服,吃不起東方肉館…
  後來到了上海,終於能買得起了,女朋友也成了老婆,然而也胖了不再買衣服了,也不再把肉食作為奢侈品了,這是多麼的幸福,沒錢的時候,買不起,有錢的時候,不用買了。
  然而事情在起變化。
  躲了老婆,躲不了情人。我想送小小一件聖誕節禮物,是的,我想了很久了。然而不知道怎麼回事,平安夜就在眼前了…中午的時候,我出去晃悠了一下,想看看能不能找點靈感,然而還是令人痛苦地失敗了。我買了一個超大的健達蛋回家,突然感受到了2007年平安夜買那串氣球的感覺。
  剛進門,就被小小堵截了,我被迫把禮物給了她,她非常之高興,我感到很慚愧,一個23塊錢的禮物在她眼裡有如此昂貴。唉,溫州老闆說我空手套白狼,我想溫州老闆是對的。都說女兒是老爸上輩子的情人,上輩子沒能給她幸福,這輩子呢?作為父親的我和女兒其實沒有任何關係,一切都是因為緣分,不管是良緣還是孽緣,讓她選擇了做我的女兒,所以一定要對她好,畢竟她本來可能是有選擇父親的權利的。
  聖誕節是基督教的節日,我也是基督教的信徒,我本該多說點,但還是選擇保持緘默,閉上眼睛,心裡禱告。眼裡有工作,心中有上帝,四海共見美好。

  願聖誕節的清晨,使我們做為您的孩子,幸福快樂,願聖誕節的夜晚引領我們來到床前,用感恩的心為你述說,赦免我的過去,赦免我的現在,因著耶穌基督的緣故,赦免我,這樣的禱告,是奉我主耶穌基督的名!阿門!

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

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述