Hi,大家好!我是CrazyCatJack。今天給大家講解Linux根文件系統的init進程和busybox的配置及編譯。 先簡單介紹一下,作為一個嵌入式系統,要想在硬體上正常使用的話。它的軟體組成大概有這三部分:1)bootloader 2)嵌入式系統kernel 3)根文件系統 。這其實非常好理 ...
Hi,大家好!我是CrazyCatJack。今天給大家講解Linux根文件系統的init進程和busybox的配置及編譯。
先簡單介紹一下,作為一個嵌入式系統,要想在硬體上正常使用的話。它的軟體組成大概有這三部分:1)bootloader 2)嵌入式系統kernel 3)根文件系統 。這其實非常好理解,類比於PC上的操作系統,首先我們需要類似BIOS的東東,來控制系統的啟動項,決定從哪裡啟動,怎樣啟動,啟動什麼。在嵌入式系統里bootloader就起著這樣的作用。再者,我們需要一個已經配置、編譯、鏈接好的內核。當然,如果自己有源碼的話,完全可以自己修改源碼,生成自己想要的內核。最後,我們需要根文件系統,在windows下,我們的系統有很多分區,那麼在嵌入式系統下,我們就需要根文件系統來完成這項工作。PC上的應用程式都是從硬碟讀取,我們嵌入式系統上的應用程式都是從根文件系統上讀取。因此,製作或修改出符合自己要求的根文件系統就變成了一件必須要做的事。今天CrazyCatJack給大家帶來的是根文件系統的分析,我們只有分析出,在嵌入式系統下,根文件系統是怎樣配置、怎樣啟動應用程式。才能修改或製作出自己的根文件系統。也就是說:要理解才行。這次博客CrazyCatJack給大家分析,那麼下一篇blog就給大家演示自己構建根文件系統了,我會儘量用簡潔明瞭的語言把自己會的知識分享給大家,讓大家最終也能做出自己的根文件系統^_^
1.原理分析
1.內容簡介
之前有博友給CrazyCatJack提建議,原理和代碼分開寫。最好還配上圖片和流程圖,因此,今天CCJ給大家配圖,相信更容易理解吧!今天講的內容如下麵的流程圖:
1)在Linux kernel的源代碼中,對如何啟動應用程式有著明確的定義。首先我們需要掛載根文件系統,只有正確掛載了根文件系統,才能夠從根文件系統中讀出應用程式。我們啟動的第一個程式就是init程式。init進程完成了對應用程式的各項配置(進程ID、執行時機、命令、終端、下一個執行的進程等),並最終依據配置執行了應用程式。
2)要執行應用程式,首先進行配置。配置文件inittab里有著對應用程式的詳細配置,這些都是C文件。init進程讀出配置、分析配置並配置應用程式、配置C庫(用到很多C庫里的函數)。最後執行程式。
3)busybox的話,其實是一個方便移植的模塊。它主要的功能在其README自述文檔中有著精煉的定義:BusyBox combines tiny versions of many common UNIX utilities into a single small executable.它的確是很方便的工具,而且本身模塊化、可配置且極具移植性。它的這一特點充分體現了busybox的存在意義和潛在價值。而且大家完全不必擔心它的執行效率。在它的自述文檔中,明白的寫著BusyBox has been written with size-optimization and limited resources in mind, both to produce small binaries and to reduce run-time memory usage.也就是說,不但將這些實用程式集結到了一個小的可執行文件中,而且它本身執行速度快,體積小,占用運行記憶體少。(怎麼感覺博主是在為busybox做廣告 -_-|| ,但誰叫它這麼給力呢)。有很多的程式都是指向busybox的。如下圖:
大家可以看到這兩個程式都指向了busybox。也就是說,執行這個命令本身和執行busybox加上這個命令的效果是一樣的。如下圖:
可能有人會問:這有什麼好的?這樣寫命令豈不是很麻煩。每次都要多加一個busybox。那請問ls你是什麼時候編譯安裝的呢?也就是說,如果不用busybox,那麼你需要自己從bin目錄下和sbin目錄下找到你要安裝的程式源代碼,手動一個一個的編譯生成文件,再安裝。而UNIX的常用工具上百個,你能一個一個的這樣做嗎?如果你可以,那在下佩服!^_^
busybox有一整套的工具,將這些你需要的工具統一編譯安裝,瞬間生成大量實用工具。
2.代碼講解
1.檢測根文件系統並啟動init
首先,在kernel源文件中的Main.c文件中,init_post函數完成了對根文件系統和控制台的檢測,並啟動init進程。代碼如下:
DIR:Main.c-init_post函數
static int noinline init_post(void) { free_initmem(); unlock_kernel(); mark_rodata_ro(); system_state = SYSTEM_RUNNING; numa_default_policy(); if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) printk(KERN_WARNING "Warning: unable to open an initial console.\n"); (void) sys_dup(0); (void) sys_dup(0); if (ramdisk_execute_command) { run_init_process(ramdisk_execute_command); printk(KERN_WARNING "Failed to execute %s\n", ramdisk_execute_command); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { run_init_process(execute_command); printk(KERN_WARNING "Failed to execute %s. Attempting " "defaults...\n", execute_command); } run_init_process("/sbin/init"); run_init_process("/etc/init"); run_init_process("/bin/init"); run_init_process("/bin/sh"); panic("No init found. Try passing init= option to kernel."); }
這裡我們看到,它首先做了一項工作:打開console文件。然後複製、複製。對應於代碼:
open("/dev/console")
(void) sys_dup(0);
(void) sys_dup(0);
也就是說打開控制台,這個控制台會對應標準輸入、標準輸出、標準錯誤。它可能是顯示器、鍵盤滑鼠等等。接著往下看:
if (execute_command) {
run_init_process(execute_command);
}
其中execute_command為命令行參數,在我們uboot傳給內核的參數中,init設置了init=/linuxrc(關於uboot的源碼講解見我之前的博客),所以這裡的execute_command就等於/linuxrc。如果傳值成功則執行run_init_process,否則列印printk(KERN_WARNING "Failed to execute %s. Attempting ""defaults...\n", execute_command);並接著往下執行,接著檢測其他位置的init進程,若成功則執行,失敗則接著往下檢測,直到找到合適的init進程或者沒找到則列印panic("No init found. Try passing init= option to kernel.");
這裡我們可以做一個實驗,檢測實際的根文件系統檢測是否和這個init_post函數定義的一樣,如果和我們分析的一致,則證明我們的分析是正確的。那麼這裡我們可以先擦除root分區,也就是說擦除根文件系統,然後啟動只有bootloader和kernel的系統。結果Linux kernel在啟動過程中,列印出瞭如下的信息:
VFS: Mounted root (yaffs filesystem). Freeing init memory: 140K Warning: unable to open an initial console. Failed to execute /linuxrc. Attempting defaults... Kernel panic - not syncing: No init found. Try passing init= option to kernel.
首先是VFS:掛載了根文件系統,可能大家會問,不是剛剛已經擦除了根文件系統,為什麼說這裡掛載了?這是因為當我們擦除了根文件系統的root分區後,Linux kernel認為它是任意格式的根文件系統(其實分區裡面什麼都沒有),而預設的又是yaffs格式,所以這裡說掛載了yaffs格式的根文件系統。這裡的warning難道不是和我們init_post函數中的printk(KERN_WARNING "Warning: unable to open an initial console.\n");相對應嗎?同理,Failed to execute /linuxrc. Attempting defaults...和printk(KERN_WARNING "Failed to execute %s. Attempting ""defaults...\n", execute_command);相對應。Kernel panic - not syncing: No init found. Try passing init= option to kernel.和
panic("No init found. Try passing init= option to kernel.");相對應。
這證明我們的分析是正確的。
2.init進程分析
在上面的內容簡介中,CCJ提到了busybox這個實用工具。這裡,我們的init程式也是由busybox編譯生成的。如果你有busybox,打開它的文件,你會發現每個小程式都有一個文件夾,裡面放的是它的編譯文件,一定會有XX.c。比如init.c、ls.c。在每個XX.c中一定有XX_main函數,定義了這個小程式將如何執行。這裡我們就打開init.c看init_main函數。init_main很長,CCJ不會把它直接拷貝過來讓大家看得眼暈,我會一點一點幫助大家分析。首先,init進程的執行順序大概是這樣的:讀取配置文件inittab(若讀取失敗則用預設配置),解析配置文件,根據配置執行應用程式。那麼這裡我們先瞧瞧inittab配置文件是怎樣定義的:
查看inittab文件得知inittab格式:
Format for each entry: # <id>:<runlevels>:<action>:<process> #id: The id field is used by BusyBox init to specify the controlling tty for the specified process to run on. #runlevels: The runlevels field is completely ignored. #action: Valid actions include: sysinit, respawn, askfirst, wait, once, # restart, ctrlaltdel, and shutdown. #process: Specifies the process to be executed and it's command line.
這是我們配置相關的格式要求。CCJ幫大家整理出來了。通俗的講,這裡的<id>就是終端,標準輸入輸出和錯誤。這裡的<runlevels>如同註釋,是完全忽略掉的。<action>是程式執行的時機,可取的值有sysinit, respawn, askfirst, wait, once,restart, ctrlaltdel, and shutdown.<process>表示將要執行的應用程式或腳本。
在init_main函數中,調用了parse_inittab函數來讀取配置文件inittab。這裡我們可以通過預設的配置語句,倒推出預設的配置文件內容,是不是很有意思?^_^我們一起來看:
DIR: init.c-parse_inittab函數
/* Reboot on Ctrl-Alt-Del */ new_init_action(CTRLALTDEL, "reboot", ""); /* Umount all filesystems on halt/reboot */ new_init_action(SHUTDOWN, "umount -a -r", ""); /* Swapoff on halt/reboot */ if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", ""); /* Prepare to restart init when a HUP is received */ new_init_action(RESTART, "init", ""); /* Askfirst shell on tty1-4 */ new_init_action(ASKFIRST, bb_default_login_shell, ""); new_init_action(ASKFIRST, bb_default_login_shell, VC_2); new_init_action(ASKFIRST, bb_default_login_shell, VC_3); new_init_action(ASKFIRST, bb_default_login_shell, VC_4); /* sysinit */ new_init_action(SYSINIT, INIT_SCRIPT, "");
這是在讀取配置文件失效時的預設配置。這裡涉及到了一個函數 new_init_action 。它實際上的工作就是把各個程式的執行時機、命令行、控制台參數分別賦值給結構體,並把這些結構體組成一個單鏈表。這也就是我們所說的配置。它的聲明是:static void new_init_action(int action, const char *command, const char *cons);這三個參數不正是inittab配置文件中的配置命令嗎?他們分別對應於<action>、<process>、<id>。按照<id>:<runlevels>:<action>:<process>的順序將參數填充進去不就是我們需要的預設配置文件嗎^_^
從預設的new_init_action反推出預設的配置文件
/* Reboot on Ctrl-Alt-Del */ new_init_action(CTRLALTDEL, "reboot", ""); ::ctrlaltdel:reboot /* Umount all filesystems on halt/reboot */ new_init_action(SHUTDOWN, "umount -a -r", ""); ::shutdown:umount -a -r /* Swapoff on halt/reboot */ if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", ""); //PC機上當記憶體不夠,就把應用程式移動到硬碟,然後讓新的應用程式讀入記憶體。在嵌入式Linux中不長用 /* Prepare to restart init when a HUP is received */ new_init_action(RESTART, "init", ""); ::restart:init /* Askfirst shell on tty1-4 */ new_init_action(ASKFIRST, bb_default_login_shell, ""); ::askfirst:-/bin/sh new_init_action(ASKFIRST, bb_default_login_shell, VC_2); tty2::askfirst:-/bin/sh new_init_action(ASKFIRST, bb_default_login_shell, VC_3); tty3::askfirst:-/bin/sh new_init_action(ASKFIRST, bb_default_login_shell, VC_4); tty4::askfirst:-/bin/sh /* sysinit */ new_init_action(SYSINIT, INIT_SCRIPT, ""); ::sysinit:/etc/init.d/rcS
其中,粉色部分的內容就是我們根據inittab配置文件格式,將C語句中new_init_action函數中的參數一一組合成預設配置文件語句,合起來就是預設配置文件的內容。這裡,CCJ認為有必要深入瞭解一下new_init_action函數。因為它是配置的核心:
DIR:init.c-new_init_action函數 static void new_init_action(int action, const char *command, const char *cons) { struct init_action *new_action, *a, *last; if (strcmp(cons, bb_dev_null) == 0 && (action & ASKFIRST)) return; /* Append to the end of the list */ for (a = last = init_action_list; a; a = a->next) { /* don't enter action if it's already in the list, * but do overwrite existing actions */ if ((strcmp(a->command, command) == 0) && (strcmp(a->terminal, cons) == 0) ) { a->action = action; return; } last = a; } new_action = xzalloc(sizeof(struct init_action)); if (last) { last->next = new_action; } else { init_action_list = new_action; } strcpy(new_action->command, command); new_action->action = action; strcpy(new_action->terminal, cons); messageD(L_LOG | L_CONSOLE, "command='%s' action=%d tty='%s'\n", new_action->command, new_action->action, new_action->terminal);
}
/* Set up a linked list of init_actions, to be read from inittab */
struct init_action {
struct init_action *next;
int action;
pid_t pid;
char command[INIT_BUFFS_SIZE];
char terminal[CONSOLE_NAME_SIZE];
};
說實話,剛開始感覺這個函數蠻繞的,CCJ根本看不懂它要幹嘛,但是我們可以寫在紙上,將它要做的工作,一步一步寫下來就明白了。不管是迴圈,還是鏈表,還是結構體。它禁不住推敲的。new_init_action函數用於配置,參數為執行時機、命令行、控制台。結構體指針new_action開始指向上一個配置過的程式(其存儲在結構體,參數是上一個程式的執行時機、命令行、控制台),這裡首先進行了一個If判斷,如果控制台等於bb_dev_null(巨集定義等於 /dev/null)且action為ASKFIRST那麼直接返回,不進行任何配置。接著這個for迴圈算是這個函數的一個重點吧,首先令結構體指針init_action_list賦值給a和last。這裡的init_action_list(巨集定義為NULL)開始為NULL,後來指向第一個配置的程式。也就是說,遍歷所有配置過的程式,如果這個程式之前被配置過(命令行和控制台同時等於當前遍歷的程式),那麼執行時機action被重新賦值為當前值。通俗的說,這個for為了避免程式重覆配置,查找之前配置過的程式有沒有當前要配置的程式,如果有,則只改變其執行時機action。命令行和控制台不變。接下來,為new_action重新分配記憶體,並且給它賦值,令它的各項信息等於當前的程式。在上面的if語句中,last->next=new_action,也就是說,將所有程式的配置結構體連城一個單鏈表。new_init_action函數講解完畢。
經過上面的講解,我們明白了Linux根文件系統中,對於程式的配置是在parse_inittab函數完成的,它打開配置文件inittab,將程式信息一一填入結構體init_action,並將它們連接成單鏈表。現在配置已經完成,下一步是執行了。接著看init_main中的代碼是怎樣執行應用程式的:
init_main程式簡要結構: init.c->init_main-> parse_inittab run_actions(SYSINIT); run_actions(WAIT); run_actions(ONCE); while (1) { run_actions(RESPAWN); run_actions(ASKFIRST); wpid = wait(NULL); while (wpid > 0) { a->pid = 0; } }
上面是簡化的init_main的程式結構,上面只有比較主要的幾個函數。第一個函數parse_inittab完成了配置。那麼下一步開始執行時機類型為sysinit, respawn, askfirst, wait, once, restart, ctrlaltdel, and shutdown類型的應用程式。那麼正如大家看到的,執行應用程式主要涉及到的是run_actions函數。這裡我們打開它:
DIR:init.c-run_actions函數 static void run_actions(int action) { struct init_action *a, *tmp; for (a = init_action_list; a; a = tmp) { tmp = a->next; if (a->action == action) { /* a->terminal of "" means "init's console" */ if (a->terminal[0] && access(a->terminal, R_OK | W_OK)) { delete_init_action(a); } else if (a->action & (SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN | RESTART)) { waitfor(a, 0); delete_init_action(a); } else if (a->action & ONCE) { run(a); delete_init_action(a); } else if (a->action & (RESPAWN | ASKFIRST)) { /* Only run stuff with pid==0. If they have * a pid, that means it is still running */ if (a->pid == 0) { a->pid = run(a); } } } } }
這裡我們以SYSINIT類型的程式為例,可以看到,在if的分支語句中,如果發現我們傳入的參數是SYSINIT,即此刻運行SYSINIT類型的應用程式。那麼waitfor(a,0),就是執行應用程式,並等待它執行完畢。具體對應於waitfor函數的run(a);(創建process子進程)和waitpid(runpid, &status, 0);(等待它結束)。執行完waitfor後,會執行delete_init_action(a);這個函數的作用是這個SYSINIT類型的程式執行完一次,就在init_action_list鏈表裡刪除。那麼其他執行時機類型的程式依此類推,前三個執行詳細如下:
SYSINIT、WAIT、ONCE時機的程式運行機制
run_actions(SYSINIT); waitfor(a, 0); //執行應用程式,等待他執行完畢 run(a); //創建process子進程 BB_EXECVP(cmdpath, cmd); waitpid(runpid, &status, 0);//等待它結束 delete_init_action(a); //執行完一次,就在init_action_list鏈表裡刪除 run_actions(WAIT); waitfor(a, 0); //執行應用程式,等待他執行完畢 run(a); //創建process子進程 BB_EXECVP(cmdpath, cmd); waitpid(runpid, &status, 0);//等待它結束 delete_init_action(a); //執行完一次,就在init_action_list鏈表裡刪除 run_actions(ONCE); run(a); //創建process子進程 delete_init_action(a); //執行完一次,就在init_action_list鏈表裡刪除 //可見init進程不會等待ONCE子進程執行完畢。
接下來的RESPAWN和ASKFIRST和上面的三個執行時機略有不同,它們處於while(1)迴圈里,並且,只有當進程的PID號等於0時才重新執行該進程。其運行機制如下:
RESPAWN和ASKFIRST運行機制 while (1) { run_actions(RESPAWN); if (a->pid == 0) { a->pid = run(a); } run_actions(ASKFIRST); if (a->pid == 0) { a->pid = run(a); } wpid = wait(NULL); //等待子進程X退出 while (wpid > 0) { a->pid = 0; //子進程X退出後,就設置X的pid等於0.然後重新進入死迴圈,再次執行這個已經退出的子進程X。也就是說哪個子進程退出,再重新執行哪個,而不是全部再執行。 } }
也就是說,只有當該進程執行完畢退出時,設置其PID為0,然後再次執行該進程。RESPAWN和ASKFIRST執行時機的程式是要重覆執行已經執行完畢的程式。對於其他程式則不重覆執行。可能大家會問,那麼RESPAWN和ASKFIRST執行時機的程式有什麼不同?大致有兩點:1)RESPAWN和ASKFIRST的執行順序不同。2)在run函數中,對於ASKFIRST類型的程式會先列印:"\nPlease press Enter to activate this console. "並且等待回車後才會繼續執行。
現在我們已經講解了SYSINIT、WAIT、ONCE、RESPAWN、ASKFIRST類型的程式。至於CTRLALTDEL類型的程式則是在init_main函數的起始部分就做了信號量的定義。也就是說,同時按下CTRL+ALT+DEL鍵,就會向內核發送信號量,並執行run_actions(CTRLALTDEL);其定義如下:
DIR:init.c-init_main函數 signal(SIGHUP, exec_signal); signal(SIGQUIT, exec_signal); signal(SIGUSR1, shutdown_signal); signal(SIGUSR2, shutdown_signal); signal(SIGINT, ctrlaltdel_signal); signal(SIGTERM, shutdown_signal); signal(SIGCONT, cont_handler); signal(SIGSTOP, stop_handler); signal(SIGTSTP, stop_handler);
shutdown類型的程式則比較複雜,因為它完成的是要關閉系統的動作。在init.c中有一個函數shutdown_system就是完成關閉系統的工作,如下:
static void shutdown_system(void) { sigset_t block_signals; /* run everything to be run at "shutdown". This is done _prior_ * to killing everything, in case people wish to use scripts to * shut things down gracefully... */ run_actions(SHUTDOWN); /* first disable all our signals */ sigemptyset(&block_signals); sigaddset(&block_signals, SIGHUP); sigaddset(&block_signals, SIGQUIT); sigaddset(&block_signals, SIGCHLD); sigaddset(&block_signals, SIGUSR1); sigaddset(&block_signals, SIGUSR2); sigaddset(&block_signals, SIGINT); sigaddset(&block_signals, SIGTERM); sigaddset(&block_signals, SIGCONT); sigaddset(&block_signals, SIGSTOP); sigaddset(&block_signals, SIGTSTP); sigprocmask(SIG_BLOCK, &block_signals, NULL); message(L_CONSOLE | L_LOG, "The system is going down NOW!"); /* Allow Ctrl-Alt-Del to reboot system. */ init_reboot(RB_ENABLE_CAD); /* Send signals to every process _except_ pid 1 */ message(L_CONSOLE | L_LOG, "Sending SIG%s to all processes", "TERM"); kill(-1, SIGTERM); sync(); sleep(1); message(L_CONSOLE | L_LOG, "Sending SIG%s to all processes", "KILL"); kill(-1, SIGKILL); sync(); sleep(1); }
大家可以看到,它先照例執行了run_actions(SHUTDOWN)。然後禁止了所有的信號傳送。列印"The system is going down NOW!" 然後關閉所有的進程,最後關閉系統。init進程講解完畢。
3.busybox的配置、編譯和安裝
首先,我們打開busybox自帶的INSTALL文件查看我們該怎樣配置、編譯和安裝busybox。
Building: ========= The BusyBox build process is similar to the Linux kernel build: make menuconfig # This creates a file called ".config" make # This creates the "busybox" executable make install # or make CONFIG_PREFIX=/path/from/root install The full list of configuration and install options is available by typing: make help
文件中寫的很明確,編譯busybox和編譯linux kernel差不多。如果大家看了我之前有關linux內核的配置、編譯和連接的博客就不會對這三條命令感到陌生了。那麼首先要make menuconfig生成配置文件.config。然後make生成busybox可執行文件。最後make install安裝busybox。首先執行 make menuconfig:
執行過後會出現圖形界面,這方便了我們對busybox的配置,這裡我們只需要手動選擇需要編譯安裝的項目,其中有很多實用的工具,最後進行保存就可以了。大家可以看到有很多的選項提供給我們。選擇後,最後退出,save即可。然後執行make生成可執行busybox文件。下一步就是安裝了。
註意:如果你是在虛擬機上安裝busybox,安裝不可直接執行make INSTALL,必須在虛擬機下自己創建一個文件夾,將安裝路徑指向這個文件夾的路徑。再執行 make CONFIG_PREFIX=/path/from/root install 否則會破壞系統。
經過簡單的配置編譯和安裝,我們最終就能使用busybox這個方便的工具了。
敬告: 本文原創,歡迎大家學習轉載^_^ 但請尊重博主CrazyCatJack的版權。 轉載請在顯著位置註明: 博主ID:CrazyCatJack |
題外話:
這大概是博主寫過最長的博客了吧。。。苦笑~寫之前沒有想到會寫這麼長,但是因為已經做出了流程圖,三個方面的內容就必須得寫下去了,自己挖的坑自己填了 T_T 。但是還是覺得很值得的。剛開始寫博客就是為了梳理自己的知識,將學過的內容鞏固,並不在乎大家是否能看懂。(實際上我懷疑是否有人會看我的博客)所以對自己寫的內容要求不是很高。但是當博主發現居然真的有人會看,那麼再這樣寫就有些對不起大家了。也有博友給博主提建議,改進博客的表現形式。博主悉心聽取了,正一步一步完善博文,方便更好的分享給大家。
博主是一路自學過來的,從51、stm32、Freescale K系列、再到現在的ARM和嵌入式Linux。看視頻,看PDF,買板子,買感測器,做項目,走到今天。相信很多博友也是和博主一樣吧。我之所以堅持到現在,就是希望創造更美好的事物。比起毀滅的力量,我更想得到創造的力量。毀滅一個事物很容易,但創造一個事物很難。不管是機器人也好、智能穿戴設備也好,創造這些insteresting、exciting、creative的東西,這是我的理想。
但是我知道一個人的力量是渺小的,雖然我做了一部分的工作,但這還遠遠不夠。我希望將知識分享給大家。集體的力量是不容小覷的。懂得分享才會有更多思維的火花碰撞,不同領域的人們彼此分享,幫助。這樣的力量是會改變時代的。就像人類大腦里的神經元,一條神經元可能只會傳達極其簡單的訊息,比如0和1。但是當這種訊息在大量的神經元間彼此傳遞,越來越多,越來越快。就會形成非常高級的事物:思想。這是一條神經元遠遠無法做到和想象得到的。
CCJ
2016-12-16 22:02:20