http://blog.csdn.net/tommy_wxie/article/details/7532628
前言Android系統(tǒng)是運作在linux kernal上的,因此它的啟動過程也遵循linux的啟動過程,當linux內(nèi)核啟動之后,運行的第一個進程是init,這個進程是一個守護進程,它的生命周期貫穿整個linux 內(nèi)核運行的始終, linux中所有其他的進程的共同始祖均為init進程。當然為了啟動并運行整個android系統(tǒng),google實現(xiàn)了自己的init進程,下面主要分析init進程都做了些什么?1.首先,init是一個守護進程,為了防止init的子進程成為僵尸進程(zombie process),需要init在子進程在結(jié)束時獲取子進程的結(jié)束碼,通過結(jié)束碼將程序表中的子進程移除,防止成為僵尸進程的子進程占用程序表的空間,當程序表的空間達到上限時,則系統(tǒng)就不能再啟動新的進程了,那么就會引起很嚴重的系統(tǒng)問題。 在linux當中,父程序是通過捕捉SIGCHLD信號來得知子進程結(jié)束的情況的;由于系統(tǒng)默認在子進程暫停時也會發(fā)送信號SIGCHLD,init需要忽略子進程在暫停時發(fā)出的SIGCHLD信號,因此將act.sa_flags 置為SA_NOCLDSTOP,該標志位的含義是就是要求系統(tǒng)在子進程暫停時不發(fā)送SIGCHLD信號。具體的代碼如下所示: struct sigaction act; ……………… act.sa_handler = sigchld_handler; act.sa_flags = SA_NOCLDSTOP; act.sa_mask = 0; act.sa_restorer = NULL; sigaction(SIGCHLD, &act, 0);2.創(chuàng)建文件系統(tǒng)目錄并掛載相關(guān)的文件系統(tǒng) /* clear the umask */ umask(0); /* Get the basic filesystem setup we need put * together in the initramdisk on / and then we'll * let the rc file figure out the rest. */ mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); mount("tmpfs", "/dev", "tmpfs", 0, "mode=0755"); mkdir("/dev/pts", 0755); mkdir("/dev/socket", 0755); mount("devpts", "/dev/pts", "devpts", 0, NULL); mount("proc", "/proc", "proc", 0, NULL); mount("sysfs", "/sys", "sysfs", 0, NULL);2.1 清除屏蔽字(file mode creation mask),保證新建的目錄的訪問權(quán)限不受屏蔽字影響.2.2 在init初始化過程中,Android分別掛載了tmpfs,devpts,proc,sysfs 4類文件系統(tǒng)2.2.1 tmpfs文件系統(tǒng) tmpfs是一種虛擬內(nèi)存文件系統(tǒng),因此它會將所有的文件存儲在虛擬內(nèi)存中,并且tmpfs下的所有內(nèi)容均為臨時性的內(nèi)容,如果你將tmpfs文件系統(tǒng)卸載后,那么其下的所有的內(nèi)容將不復(fù)存在。 tmpfs有些像虛擬磁盤(ramdisk),但不是一回事。說其像虛擬磁盤,是因為它可以使用你的RAM,但它也可以使用你的交換分區(qū)。傳統(tǒng)的虛擬磁盤是一個塊設(shè)備,而且需要一個mkfs之類的命令格式化它才能使用。tmpfs是一個獨立的文件系統(tǒng),不是塊設(shè)備,只要掛接,立即就可以使用。 tmpfs的大下是不確定的,它最初只有很小的空間,但隨著文件的復(fù)制和創(chuàng)建,它的大小就會不斷變化,換句話說,它會根據(jù)你的實際需要而改變大??;tmpfs的速度非常驚人,畢竟它是駐留在RAM中的,即使用了交換分區(qū),性能仍然非常卓越;由于tmpfs是駐留在RAM的,因此它的內(nèi)容是不持久的,斷電后,tmpfs的內(nèi)容就消失了,這也是被稱作tmpfs的根本原因。 關(guān)于tmpfs文件系統(tǒng)請參考linux內(nèi)核文檔: kernel/Documentation/filesystems/tmpfs.txt2.2.2 devpts文件系統(tǒng) devpts文件系統(tǒng)為偽終端提供了一個標準接口,它的標準掛接點是/dev/pts。只要pty的主復(fù)合設(shè)備/dev/ptmx被打開,就會在/dev/pts下動態(tài)的創(chuàng)建一個新的pty設(shè)備文件。2.2.3 proc文件系統(tǒng) proc文件系統(tǒng)是一個非常重要的虛擬文件系統(tǒng),它可以看作是內(nèi)核內(nèi)部數(shù)據(jù)結(jié)構(gòu)的接口,通過它我們可以獲得系統(tǒng)的信息,同時也能夠在運行時修改特定的內(nèi)核參數(shù)。 在proc文件系統(tǒng)中,你可以修改內(nèi)核的參數(shù),是不是很強大?怎么修改呢?你只需要echo一個新的值到對應(yīng)的文件中即可,但是如果在修改過程中發(fā)生錯誤的話,那么你將別無選擇,只能重啟設(shè)備。 關(guān)于tmpfs文件系統(tǒng)請參考linux內(nèi)核文檔: kernel/Documentation/filesystems/proc.txt2.2.4 sysfs文件系統(tǒng) 與proc文件系統(tǒng)類似,sysfs文件系統(tǒng)也是一個不占有任何磁盤空間的虛擬文件系統(tǒng)。它通常被掛接在/sys目錄下。sysfs文件系統(tǒng)是Linux2.6內(nèi)核引入的,它把連接在系統(tǒng)上的設(shè)備和總線組織成為一個分級的文件,使得它們可以在用戶空間存取。3.屏蔽標準的輸入輸出,即標準的輸入輸出定向到NULL設(shè)備。 這一步是通過調(diào)用函數(shù)open_devnull_stdio實現(xiàn)的,下面我們研究一下open_devnull_stdio的函數(shù)實現(xiàn)void open_devnull_stdio(void){ int fd; static const char *name = "/dev/__null__";//創(chuàng)建一個字符專用文件(character special file) /dev/__null__ if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) {//獲取/dev/__null__的文件描述符,并輸出該文件 fd = open(name, O_RDWR); unlink(name);//將與進程相關(guān)的標準輸入(0),標準輸出(1),標準錯誤輸出(2),均定向到NULL設(shè)備 if (fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd > 2) { close(fd); } return; } } exit(1);} 這里解釋一下 dup2(fd, 0); dup2(fd, 1); dup2(fd, 2);過程:首先說明以下dup2的作用,這個函數(shù)主要是復(fù)制一個函數(shù)的描述符,一般用于重定向進程的stdin,stdout,stderr。它的原型如下:int dup2(int oldfd, int newfd); dup2(fd, 0); dup2(fd, 1); dup2(fd, 2);這三次調(diào)用一次將依次代表stdin,stdout,stderr的描述符0,1,2,重定向到dev/null,通過這種方式達到屏蔽標準輸入輸出的作用。4. 初始化內(nèi)核log系統(tǒng) 這個過程對應(yīng)的源碼為:log_init();這個函數(shù)詳細實現(xiàn)為void log_init(void){ static const char *name = "/dev/__kmsg__"; if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) { log_fd = open(name, O_WRONLY);//當進程在進行exec系統(tǒng)調(diào)用時,要確保log_fd是關(guān)閉的(通過FD_CLOEXEC標志位來設(shè)置). fcntl(log_fd, F_SETFD, FD_CLOEXEC); unlink(name); }}有上述實現(xiàn)看出內(nèi)核的log輸出是通過文件描述符log_fd寫入的,那到底寫入到什么設(shè)備呢?/dev/kmsg,這個設(shè)備則會把它收到的任何寫入都作為printk的輸出。printk函數(shù)是內(nèi)核中運行的向控制臺輸出顯示的函數(shù)。5.解析init.rc5.1 Android init language Android init language包含四種類型語句:Actions, Commands, Services, Options。它的主要語法風格為: 1.每一個語句占據(jù)一行,所有關(guān)鍵字通過空格來分割。 2.c語言風格的反斜杠(/)將被轉(zhuǎn)義為插入一個空格; 3.如果一個關(guān)鍵字含有一個或多個空格,那么怎么保證關(guān)鍵字完整呢?可以使用雙引號來確定關(guān)鍵字的范圍。 4.用于行尾的反斜杠表示續(xù)行符。 5.Actions和Services聲明一個字段(section),緊隨其后的Commands和Options均屬于這個字段,在第一個字段之前的Commands和Options的沒有意義。 6.Actions和Services有獨一無二的名字,如果Actions和Services的名字有重名,那么將被視作錯誤。5.1.1 Actions Actions其實就是一組被命名的Commands序列。當滿足觸發(fā)器的事件發(fā)生時,這個action就會被置于一個隊列中,這個隊列存放著將要被執(zhí)行的action。其格式如下: on <trigger> <command> <command> <command> on是Actions的關(guān)鍵字,它表明下面的序列是Actions序列。5.1.2 Services Services是有init進程啟動的或者重新啟動的程序。其格式如下: service <name> <pathname> [ <argument> ]* <option> <option>5.1.3 Options Options是Services的修飾符,由它來指定何時并且如何啟動Services程序。5.1.4 Commands Commands即是在滿足triger條件后,Actions中執(zhí)行的內(nèi)容。Options和Commands的取值在這里就不描述里,有興趣請參考system/core/rootdir/init.rc5.2 init.rc解析過程 我們繼續(xù)回到init.c的main函數(shù)中,看init.rc的解析過程。init文件有兩個init.rc和init.hardware.rc。 init_parse_config_file("/init.rc");//解析init.rc /* pull the kernel commandline and ramdisk properties file in */ import_kernel_cmdline(0);//從/proc/cmdline讀取內(nèi)核啟動參數(shù),并保存到相應(yīng)的變量中 get_hardware_name(hardware, &revision);//從/proc/cpuinfo中獲取硬件信息 snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware); init_parse_config_file(tmp);//解析硬件相關(guān)的init信息 著重介紹一下init_parse_config_file過程,這個函數(shù)負責init文件的解析。 1.首先判斷關(guān)鍵字,只能有兩種可能on或者service,通過關(guān)鍵字來判定section范圍; 2.根據(jù)Actions和Services的格式對section進行逐行解析; 3.將解析出的內(nèi)容存放到雙向循環(huán)鏈表中。 解析過程中的雙向循環(huán)鏈表的使用,android用到了一個非常巧妙的鏈表實現(xiàn)方法,一般情況下如果鏈表的節(jié)點是一個單獨的數(shù)據(jù)結(jié)構(gòu)的話,那么針對不同的數(shù)據(jù)結(jié)構(gòu),都需要定義不同鏈表操作。 而在初始化過程中使用到的鏈表則解決了這個問題,它將鏈表的節(jié)點定義為了一個非常精簡的結(jié)構(gòu),只包含前向和后向指針,那么在定義不同的數(shù)據(jù)結(jié)構(gòu)時,只需要將鏈表節(jié)點嵌入到數(shù)據(jù)結(jié)構(gòu)中即可。 例如,鏈表節(jié)點定義如下, struct listnode { struct listnode *next; struct listnode *prev; }; 數(shù)據(jù)結(jié)構(gòu)的定義如下,拿Action的數(shù)據(jù)結(jié)構(gòu)為例, struct action { /* node in list of all actions */ struct listnode alist; /* node in the queue of pending actions */ struct listnode qlist; /* node in list of actions for a trigger */ struct listnode tlist; unsigned hash; const char *name; struct listnode commands; struct command *current; }; 這樣的話,所有的鏈表的基本操作,例如插入,刪除等只會針對listnode進行操作,而不是針對特定的數(shù)據(jù)結(jié)構(gòu),如action進行操作,那么在多個數(shù)據(jù)結(jié)構(gòu)使用雙向鏈表時,鏈表的實現(xiàn)得到了統(tǒng)一,即精簡了代碼,又提高了效率。 但是這樣的鏈表實現(xiàn),存在一個問題,鏈表節(jié)點listnode中只有前向和后向指針,并且前向和后向指針均指向listnode,那么我們通過什么方式來訪問數(shù)據(jù)結(jié)構(gòu)action的內(nèi)容呢? 在這里引入了一個宏offsetof,我們man一下這個宏的的定義,發(fā)現(xiàn)這個宏是結(jié)構(gòu)體中成員變量的偏移量。這下大家心里是不是已經(jīng)意識到怎么訪問數(shù)據(jù)結(jié)構(gòu)action了吧,對!就是計算鏈表節(jié)點在數(shù)據(jù)結(jié)構(gòu)中的偏移量,來計算數(shù)據(jù)結(jié)構(gòu)實例的地址。 Android的init過程是通過下面的宏定義來實現(xiàn)的,#define node_to_item(node, container, member) / (container *) (((char*) (node)) - offsetof(container, member)) 小結(jié)一下這種鏈表的優(yōu)點:(1)所有鏈表基本操作都是基于listnode指針的,因此添加類型時,不需要重復(fù)寫鏈表基本操作函數(shù)(2)一個container數(shù)據(jù)結(jié)構(gòu)可以含有多個listnode成員,這樣就可以同時掛到多個不同的鏈表中。5.3 Actions待執(zhí)行隊列 當解析完所有的init.rc內(nèi)容之后,在執(zhí)行這些action之前,需要按順序?qū)⑵渲糜谝粋€待執(zhí)行隊列中,如 action_for_each_trigger("early-init", action_add_queue_tail); 還有一些沒有在init.rc中定義的action,相比init.rc,這些action的共同點是沒有參數(shù),如 queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done"); 下面我們分析一下init中的Actions待執(zhí)行隊列的順序以及功能 action_for_each_trigger("early-init", action_add_queue_tail); queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done"); queue_builtin_action(property_init_action, "property_init"); queue_builtin_action(keychord_init_action, "keychord_init"); queue_builtin_action(console_init_action, "console_init"); queue_builtin_action(set_init_properties_action, "set_init_properties"); /* execute all the boot actions to get us started */ action_for_each_trigger("init", action_add_queue_tail); action_for_each_trigger("early-fs", action_add_queue_tail); action_for_each_trigger("fs", action_add_queue_tail); action_for_each_trigger("post-fs", action_add_queue_tail); queue_builtin_action(property_service_init_action, "property_service_init"); queue_builtin_action(signal_init_action, "signal_init"); queue_builtin_action(check_startup_action, "check_startup"); /* execute all the boot actions to get us started */ action_for_each_trigger("early-boot", action_add_queue_tail); action_for_each_trigger("boot", action_add_queue_tail); /* run all property triggers based on current state of the properties */ queue_builtin_action(queue_property_triggers_action, "queue_propety_triggers");#if BOOTCHART queue_builtin_action(bootchart_init_action, "bootchart_init");#endif 5.3.1 early-init 查看init.rc中的相應(yīng)字符段為 start ueventd 這個action主要目的是通過early-init啟動ueventd服務(wù),這個服務(wù)負責uevent(user space event)的處理,uevent是內(nèi)核向用戶空間發(fā)出的一個時間通知,使應(yīng)用程序能夠有機會對該event做出反應(yīng)。5.3.2 wait_for_coldboot_done android 冷過程結(jié)束后會生成dev/.coldboot_done文件,wait_for_coldboot_done這個action會等待dev/.coldboot_done文件的生成,等待時長為5s。當然這個action不會阻塞android的冷啟動過程,它會沒查詢一次就會休眠0.1s,直到冷啟動結(jié)束。5.3.3 property_init 幾種特殊的屬性: 1.ro.屬性,它表示只讀屬性,它一旦被設(shè)置就不能被修改; 2.net.屬性,顧名思義,就是與網(wǎng)絡(luò)相關(guān)的屬性,net.屬性中有一個特殊的屬性:net.change,它記錄了每一次最新設(shè)置和更新的net.屬性,也就是每次設(shè)置和更新net.屬性時則會自動的更新net.change屬性,net.change屬性的value就是這個被設(shè)置或者更新的net屬性的name。例如我們更新了屬性net.bt.name的值,由于net有屬性發(fā)生了變化,那么屬性服務(wù)就會自動更新net.change,將其值設(shè)置為net.bt.name。 3.persist.屬性,以文件的形式保存在/data/property路徑下。persist.屬性由于將其保存在了用戶空間中,所以在property_init中是不能對其更新的,只能將其更新過程交給用戶來處理。 4.ctl.屬性,雖然是以屬性的形式來進行設(shè)置,其實它的目的是為了啟動或關(guān)閉它指定的service 初始化android的屬性系統(tǒng),整個的過程分為下面2步 1.初始化屬性區(qū)域(property area),主要工作是將屬性設(shè)備節(jié)點/dev/properties映射到內(nèi)存空間上,將整個的屬性內(nèi)容作為共享內(nèi)存來處理,這個共享內(nèi)存就是屬性區(qū)域,當前android中使用全局變量__system_property_area__來標記屬性區(qū)域。 2.加載并設(shè)置/default.prop中定義的屬性,default.prop中主要是一些“ro.”只讀屬性。5.3.4 keychord_init 這個東東不是太理解,目前的所有service均未用到這個機制。5.3.5 console_init 1.如果/proc/cmdline指定了控制臺終端,那么優(yōu)先使用這個控制臺,如果沒有指定,那么將使用默認控制臺終端/dev/console。 2.加載開機圖片,參考load_565rle_image函數(shù) a,通過ioctl函數(shù)修改dev/tty0(即終端控制臺)為圖像顯示模式; b,嘗試打開/initlogo.rle,如果失敗,那么將dev/tty0恢復(fù)為文本顯示模式,則開機時顯示"ANDROID"文字; c,如果打開/initlogo.rle成功,那么init將會打開Framebuffer,下面我們分析一下這個過程 //logo.c static int fb_open(struct FB *fb) { //打開Framebuffer對應(yīng)的設(shè)備文件/dev/graphics/fb0 fb->fd = open("/dev/graphics/fb0", O_RDWR); if (fb->fd < 0) return -1; //通過ioctl函數(shù)獲得Framebuffer相關(guān)信息 //FBIOGET_FSCREENINFO對應(yīng)的是Framebuffer的固定信息 //FBIOGET_VSCREENINFO對應(yīng)的是Framebuffer的可變信息 if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->fi) < 0) goto fail; if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vi) < 0) goto fail; //由于Framebuffer是可以被用戶直接讀寫的,所以需要將/dev/graphics/fb0映射到用戶空間的內(nèi)存區(qū)。 fb->bits = mmap(0, fb_size(fb), PROT_READ | PROT_WRITE, MAP_SHARED, fb->fd, 0); if (fb->bits == MAP_FAILED) goto fail; return 0; fail: close(fb->fd); return -1; } d,將initlogo.rle數(shù)據(jù)寫到Framebuffer中。 目前android默認是沒有initlogo.rle,如果想自己添加開機圖片的話,具體過程請參考http://www.cnmsdn.com/html/201005/1274855679ID5109.html5.3.6 set_init_properties 設(shè)置與硬件載頻相關(guān)的只讀屬性。5.3.7 init 執(zhí)行init.rc中init action字段中定義的處理。init.rc中的actions就不再一一分析了,有興趣或者有時間在分析。5.3.8 property_service_init 1.讀取/system/build.prop,/system/default.prop, /data/local.prop以及/data/property/下的屬性并將其設(shè)置; 2.創(chuàng)建一個服務(wù)器端UNIX Domain Socket,它的socket文件路徑為/dev/socket/property_service,這個socket監(jiān)聽來自客戶端的屬性修改請求.5.3.9 signal_init 1. 2.通過socketpair創(chuàng)建一對已連接的socket,將生成的兩個socket設(shè)置為O_NONBLOCK模式,也就是將對socket句柄的讀寫操作設(shè)置為非阻塞模式。5.3.10 check_startup 確保5.3.8中屬性設(shè)置socket文件描述符和signal_init中signal socket文件描述符,如果兩個有其一不存在,那么將退出系統(tǒng)。5.3.11 boot boot action主要由兩部分組成, 1. 還是一些配置性的工作,例如基本的網(wǎng)絡(luò)配置;ActivityManagerService中用到的進程管理和資源回收時,需要用到的優(yōu)先級變量的設(shè)置等。 2. 啟動所有init.rc聲明的未指定class的service; 具體的command為 class_start default。 在解析init.rc時,如果service未指定class選項的話,那么會給它的classname默認的指定為“default”,而目前的init.rc中的所有的service均未指定class選項,所以命令“class_start default”將按順序啟動所有的service。 也可以為需要一起啟動,一起關(guān)閉的services指定一個相同的class,那么就可以對這些service進行統(tǒng)一處理了。 還需注意:如果service中定義了disabled選項,那么不能通過class_start來啟動它,只能顯示的一個一個的啟動。被disabled修飾的service一般是在5.3.12 queue_propety_triggers 根據(jù)init.rc中action指定的property值與屬性中的值比較,如果相等則執(zhí)行對應(yīng)的command。例如 on property:ro.secure=0 start console 如果當前ro.secure的值為0,那么啟動console服務(wù)5.3.13 bootchart_init Bootchart 能夠?qū)ο到y(tǒng)的性能進行分析,并生成系統(tǒng)啟動過程的圖表,以便為你提供有價值的參考信息。綜合所得的信息,你就可以進行相應(yīng)的改進,從而加快你的 Linux 系統(tǒng)啟動過程。 如果設(shè)置了Bootchart,則該過程初始化Bootchart。5.4 init輪詢過程 以上部分將所有需要操作的action均放在了action待執(zhí)行隊列中,那么init進程將要進入一個死循環(huán)過程,整個android的將會運行在這個生命周期內(nèi)。 1.執(zhí)行action待執(zhí)行隊列中的所有command; 2.重啟所有需要重啟的service; 3.注冊屬性設(shè)置property_set_fd,信號signal處理signal_recv_fd,keychord keychord_fd三個文件描述符的為輪詢對象。 if (!property_set_fd_init && get_property_set_fd() > 0) { ufds[fd_count].fd = get_property_set_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; property_set_fd_init = 1; } if (!signal_fd_init && get_signal_fd() > 0) { ufds[fd_count].fd = get_signal_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; signal_fd_init = 1; } if (!keychord_fd_init && get_keychord_fd() > 0) { ufds[fd_count].fd = get_keychord_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; keychord_fd_init = 1; } 有以上代碼可見,init進程將三個描述符均定義為了POLLIN事件響應(yīng),當描述符有可讀數(shù)據(jù)時,對于socket描述符,有連接請求時ufds就會收到POLLIN事件。 4.下面分別對這3個文件描述符的輪詢過程作簡單的介紹 nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents == POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } 上面的代碼為輪詢的總體體現(xiàn),當有POLLIN事件發(fā)生時,相應(yīng)的ufds[i].revents就會被置為POLLIN,然后執(zhí)行各自的handler A,property_set_fd 收到屬性設(shè)置的socket請求之后,設(shè)置相關(guān)屬性。 B,signal_recv_fd 當有子進程終止時,也就是service終止時,內(nèi)核會給init發(fā)送SIGCHLD,此時調(diào)用注冊的handler函數(shù) static void sigchld_handler(int s) { write(signal_fd, &s, 1); } 這個handler函數(shù)是向其中的一個socket signal_fd寫入數(shù)據(jù),由于signal_init過程中初始化了一對已連接的socket signal_fd和signal_recv_fd,因此此時signal_recv_fd會收到向signal_fd寫入的數(shù)據(jù),然后查詢那個service終止,然后根據(jù)該service的屬性來作相關(guān)的操作,是重啟還是結(jié)束進行資源回收。 C,keychord_fd 目前的init過程中沒有service執(zhí)行keychord機制。
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請
點擊舉報。