Linux Container 研究报告
1. 綜述
lxc是Linux Container的用戶態(tài)工具包。其代碼由三部分組成:
在命名習慣上, 生成lxc命令的c源文件都有l(wèi)xc_的前綴。而生成liblxc.so的c源文件則沒有該前綴。
2. lxc-start
lxc-start的執(zhí)行過程大致就是兩步:
2.1 解析命令行和配置文件
lxc-start的main函數(shù)在lxc_start.c中。從main的流程中能大致窺探出lxc-start的執(zhí)行過程。首先調(diào)用lxc_arguments_parse來分析命令行,再調(diào)用lxc_config_read來解析配置文件,等獲取好足夠的信息后,調(diào)用lxc_start開始了container。
lxc_arguments_parse
函數(shù)的實現(xiàn)位于arguments.c中。通過該函數(shù)的執(zhí)行,命令行參數(shù)中的信息被存放到了類型為lxc_arguments的變量my_args中。命令行參數(shù)中有幾個我們可以注意一下:
- "-n" 指定了container的名字
- "-c" 指定了某個文件作為container的console
- "-s" 在命令行中指定key=value的config選項
- "-o" 指定輸出的log文件
- "-l" log的打印級別。用"-l DEBUG"命令行參數(shù),log打印的信息最詳細
lxc_arguments中有一個字段lxcpath指定了lxc container的存放路徑。可以在命令參數(shù)-P選項中設置。如果不設置,會啟用默認參數(shù)。該默認參數(shù)是在編譯lxc代碼的時候指定的,一般情況下為"/usr/local/var/lib/lxc"或"/var/lib/lxc"。在本文中,我們一律用LXCPATH來表示該路徑。
另外,我們約定用CT-NAME表示-n參數(shù)指定的container的名稱。
lxc_config_read
它的實現(xiàn)在confile.c中。逐行讀取配置文件。并調(diào)用confile.c下的parse_line來每行做解析。因為配置文件每行都是key=value的形式的,所以parse_line的主要內(nèi)容是找到"=",分析出(key, value)對并做處理。
配置文件的路徑由-f參數(shù)指定。不指定則從LXCPATH/CT-NAME/config中讀取。
confile.c的真正核心內(nèi)容在與95行定義的一個結(jié)構(gòu)體數(shù)組
static struct lxc_config_t config[] = {{ "lxc.arch", config_personality },{ "lxc.pts", config_pts },{ "lxc.tty", config_tty },{ "lxc.devttydir", config_ttydir },..... };其中l(wèi)xc_config_t結(jié)構(gòu)體的定義如下:
typedef int (*config_cb)(const char *key, const char *value, struct lxc_conf *lxc_conf); struct lxc_config_t {char* name;config_cb cb; };lxc_config_t結(jié)構(gòu)體的name字段給定了key的值,回調(diào)函數(shù)cb給出了對應key的處理方法。整個config結(jié)構(gòu)體數(shù)組就是鍵值與對應動作的一個查找表。
現(xiàn)在我們來考慮一個場景。我們在配置文件中寫入了lxc.tty = 4。parse_line會解析出(lxc.tty, 4)的序?qū)Α5絚onfig中查詢得出其對應的處理函數(shù)是config_tty,于是就開始調(diào)用config_tty。
函數(shù)指針config_cb一共有三個參數(shù),key指的是如lxc.tty之類的鍵,而value指的是如4之類的值。lxc_conf用來存放config文件的分析結(jié)果。
lxc_config_read讀取好的配置文件放在類型為lxc_conf的結(jié)構(gòu)體指針conf。lxc_conf的定義在conf.h中。
2.2 調(diào)用lxc_start
讓我們再次回到main函數(shù)。前面我們通過分析命令行參數(shù)和配置文件,收集了container的一系列信息,接下來就該啟動container了。
main函數(shù)255行的lxc_start打響了了啟動container的第一槍。lxc_start的實現(xiàn)在start.c中,函數(shù)原型如下:
int lxc_start(const char* name, char *const argv[], struct lxc_conf *conf, const char *lxcpath)四個參數(shù)的含義如下:
- name?CT-NAME
- argv container要執(zhí)行的第一個命令。可以通過命令行參數(shù)指定, 如“l(fā)xc-start -n android4.2 /init”,這里的argv就會是{"/init", NULL}。如果沒有指定,默認是{"/sbin/init", NULL}
- conf 前面解析好的container配置文件中指定的配置信息。
- lxcpath LXCPATH
lxc_start首先調(diào)用lxc_check_inherited來關閉所有打開的文件句柄。0(stdin), 1(stdout), 2(stder)和日志文件除外。緊接著就調(diào)__lxc_start。
__lxc_start
其原型如下:
int __lxc_start(const char* name, struct lxc_conf *conf, struct lxc_operators *op, void *data, const char *lxcpath)lxc_operators結(jié)構(gòu)體定義如下:
struct lxc_operations {int (*start)(struct lxc_handler *, void *);int (*post_start)(struct lxc_handler *, void *); };各個參數(shù)含義如下:
- name?CT-NAME
- conf container配置信息
- op 用lxc_operator結(jié)構(gòu)體來存放了兩個函數(shù)指針start和post_start。這兩個指針分別指向start.c的start函數(shù)和post_start函數(shù)。
- data start_args類型的結(jié)構(gòu)體,唯一的成員變量argv指向了lxc_start的實參argv,也就是container要執(zhí)行的init。
- lxcpath?LXCPATH
__lxc_start代碼不復雜。我們將比較重要的幾個函數(shù)調(diào)用抽出來看。
lxc_init
__lxc_start調(diào)用的第一個函數(shù),用來初始化lxc_handler結(jié)構(gòu)體。傳入的三個參數(shù)依次為:
- name?CT-NAME
- conf container的配置信息
- lxcpath?LXCPATH
函數(shù)先新分配一個lxc_handler的結(jié)構(gòu)體handler,設置其conf、lxcpath和name字段。然后調(diào)用了lxc_command_init新創(chuàng)建了一個socket并listen之,新建socket的句柄放置在handler->maincmd_fd中。該socket的作用應為接受外部命令。lxc_command_init的實現(xiàn)在commands.c中。其分析可以詳見模塊commands.c部分。
接著是lxc_set_state。它將STARTING的狀態(tài)消息寫入到另一個socket中。lxc_set_state的實現(xiàn)調(diào)用了monitor.c的lxc_moitor_send_state。對其分析可以參見monitor.c模塊。
接著是部分環(huán)境變量的設置:LXC_NAME, LXC_CONFIG_FILE, LXC_ROOTFS_MOUNT, LXC_ROOTFS_PATH, LXC_CONSOLE, LXC_CONSOLE_LOGPATH。
接下來的四件事情:
我們來重點分析第二步和第三步
終端設備的創(chuàng)建
1. tty
lxc_create_tty通過調(diào)用openpty的命令來為container分配tty設備。conf->tty參數(shù)指定了要分配的tty的個數(shù)。conf->tty_info結(jié)構(gòu)體用來存放分配好的tty的相關信息。
如果conf->tty的值是4,那么lxc_create_tty執(zhí)行完之后的結(jié)果是:
conf->tty_info->nb_tty: 4 conf->tty_info->pty_info: 大小為4的類型為lxc_pty_info的數(shù)組的頭指針lxc_pty_info的定義如下:
struct lxc_pty_info {char name[MAXPATHLEN];int master;int slave;int busy; };conf->tty_info->pty_info的每一項都記錄了一個新創(chuàng)建pty的信息,master表示pty master的句柄,slave表示slave的句柄,name表示pty slave的文件路徑,即"/dev/pts/N"。
2. console
lxc_create_console同樣調(diào)用openpty用來創(chuàng)建console設備。創(chuàng)建好的console設備信息存放在類型為lxc_cosnole的結(jié)構(gòu)體變量conf->console中。
lxc_console的結(jié)構(gòu)體定義如下:
struct lxc_console {int slave;int master;int peer;char *path;char *log_path;int log_fd;char name[MAXPATHLEN];struct termios *tios; };各個參數(shù)的含義如下:
- slave 新創(chuàng)建pty的slave
- master 新創(chuàng)建pty的master
- path console文件路徑,可以通過配置文件"lxc.console.path"或者命令行參數(shù)-c指定。默認為"/dev/tty"
- log_path console的日志路徑
- peer 打開path, 返回句柄放入peer中。
- log_fd 打開log_path, 返回句柄放入log_fd中。
- name slave的路徑。
- tios 存放tty舊的控制參數(shù)。
lxc_spawn
讓我們繼續(xù)回到__lxc_start的主流程。前面通過調(diào)用lxc_init初始化了一個lxc_handler的結(jié)構(gòu)體handler,然后在主流程里,又將傳入的ops參數(shù)和data參數(shù)賦值給了handler的ops字段和data字段。接著就以handler為參數(shù),調(diào)用了lxc_spawn。
lxc_spawn是啟動新的容器的核心。進程通過Linux系統(tǒng)調(diào)用clone創(chuàng)建了擁有自己的PID、IPC、文件系統(tǒng)等獨立的命名空間的新進程。然后在新的進程中執(zhí)行/sbin/init。接下來我們來看具體過程。
- CLONE_NEWUTS?子進程指定了新的utsname,即新的“計算機名”
- CLONE_NEWPID?子進程擁有了新的PID空間,clone出的子進程會變成1號進程
- CLONE_NEWIPC?子進程位于新的IPC命名空間中。這樣SYSTEM V的IPC對象和POSIX的消息隊列看上去會獨立于原系統(tǒng)。
- CLONE_NEWNS?子進程會有新的掛載空間。
- CLONE_NEWNET?如果配置文件中有關于網(wǎng)絡的配置,則會增加該flag。它使得子進程有了新的網(wǎng)絡設備的命名空間
我們先來看一下lxc_clone是如何創(chuàng)建新的進程的。
lxc_clone
該函數(shù)的原型如下:
pid_t lxc_clone(int (*fn)(void *), void *arg, int flags)三個參數(shù)的含義如下:
fn: 子進程要執(zhí)行的函數(shù)入口 arg:fn的輸入?yún)?shù) flags: clone的flagslxc_clone為clone api做了一個簡單的封裝,最后結(jié)果就是子進程會執(zhí)行fn(arg)。在lxc_spawn處,lxc_clone是這樣調(diào)用的:
handler->pid = lxc_clone(do_start, handler, handler->clone_flags)所以lxc_clone執(zhí)行完后,handler的pid字段會保留子進程的pid(注意不是“1”,是子進程調(diào)用getpid()會變成1)。父進程繼續(xù),子進程執(zhí)行do_start。
父子進程同步
同步機制
進程間同步的函數(shù)實現(xiàn)在sync.c中,其實現(xiàn)機制的分析可以見sync.c模塊。這里只列舉用于同步的函數(shù):
- lxc_sync_barrier_parent/child(struct lxc_handler* handler, int sequence)?發(fā)送sequence給parent/child,同時等待parent/child發(fā)送sequence+1的消息過來。
- lxc_sync_wait_parent/child(struct lxc_handler* handler, int sequence)?等待parent/child發(fā)送sequence的消息過來。
同步完成配置的過程
相關配置
在這一部分中,我們針對前面講的父子進程的同步配置過程來對部分重要的函數(shù)做分析。
lxc_cgroup_path_create和lxc_cgroup_enter
函數(shù)定義在cgroup.c中。原型如下:
char* lxc_cgroup_path_create(const char* lxcgroup, const char* name) int lxc_cgroup_enter(const char* cgpath, pid_t pid)lxc_cgroup_path_create函數(shù)的作用是在cgroup各個已掛載使用的子系統(tǒng)的掛載點上為新創(chuàng)建的container新建一個文件夾。lxc_cgroup_enter的作用是把新創(chuàng)建的container加入到group cgpath中。
下面我們來舉例說明: 比如掛載的子系統(tǒng)有blkio和cpuset,他們的掛載點分別是/cgroup/blkio和/cgroup/cpuset。
lxc_cgroup_path_create函數(shù)運行結(jié)束后,則會多出兩個目錄/cgroup/blkio/lxcgroup/name和/cgroup/cpuset/lxcgroup/name。如果傳入?yún)?shù)lxcgroup為空,則會使用“l(fā)xc”。函數(shù)的返回值是新創(chuàng)建目錄的相對路徑。即“l(fā)xcgroup/name”。
lxc_cgroup_enter函數(shù)結(jié)束后,進程號"pid"會被追加到文件/cgroup/blkio/lxcgroup/name/tasks和/cgrop/cpuset/lxcgroup/name/tasks中。在lxc_spawn中,pid的實參用的是handler->pid,即clone出的子進程的id。
通過查看/proc/mounts查看cgroup的掛載點。通過查看/proc/cgroups查看正掛載使用的子系統(tǒng)。
id映射
查看confile.c下的config_idmap函數(shù),可知在container配置文件中可以通過lxc.id_map來設置id映射。格式如下:
lxc.id_map = u/g id_inside_ns id_outside_ns range其中,u/g指定了是uid還是gid。后三個選項表示container中的[id_inside_ns, id_inside_ns+range)會被映射到真實系統(tǒng)中的 [id_outside_ns, id_outside_ns+range)。
在config_idmap執(zhí)行后,配置文件中的配置條目被作為鏈表存放到conf->id_map字段下。在lxc_spawn中,通過調(diào)用lxc_map_ids函數(shù)來完成配置。
lxc_map_ids的實現(xiàn)在conf.c中,原型如下:
int lxc_map_ids(struct lxc_list *idmap, pid_t pid)第一個參數(shù)idmap為配置信息的鏈表,pid為新clone出的子進程的pid。lxc_map_ids的基本過程比較簡單,就是將以u開頭的配置項寫入到文件/proc/pid/uid_map中,將以g開頭的配置項寫入到文件/proc/pid/gid_map中。
id映射是clone在flag CLONE_NEWUSER時指定的一個namespace特性。有關id映射可以參見此處。
lxc_setup
子進程do_start中調(diào)用的lxc_setup是一個非常重要的函數(shù)。container里面的很多配置都是在lxc_setup中完成的。這里重點分析setup_console和setup_tty兩個函數(shù),來查看比較困擾的終端字符設備是如何虛擬的。
setup_console的實現(xiàn)在conf.c中,函數(shù)原型如下:
int setup_console(const struct lxc_rootfs *rootfs, const struct lxc_console *console, char *ttydir)rootf變量描述了container根文件系統(tǒng)的路徑和掛載點。console變量描述了container的console設備的相關信息。在do_start的調(diào)用中,傳來的實參是lxc_conf->console,這個字段是我們在lxc_init時初始化的。回顧當時的初始化過程:
- console->slave和console->master分別存儲了新分配的pty的slave和master的句柄。
- console->peer存儲了打開原系統(tǒng)"/dev/tty"的句柄。
- console->name指向了新分配pty的文件路徑
setup_console根據(jù)ttydir的值,分支調(diào)用了setup_dev_console或者setup_ttydir_console。我們只看setup_dev_console來了解原理。
在setup_dev_console中,主要動作就是將console->name指向的pty通過BIND的方式掛載到rootfs/dev/console文件中。可以看出,我們對container /dev/console的訪問,實質(zhì)上是對新分配pty的訪問。
setup_tty的過程與此類似,在rootfs/dev/目錄下創(chuàng)建tty1, tty2等常規(guī)文件,然后用BIND的方式將新創(chuàng)建的pty掛載到其上。
setup_cgroup
顯然,container無法訪問原來的cgroup根文件系統(tǒng),所以這個任務只能由父進程在lxc_spawn中調(diào)用。該函數(shù)實現(xiàn)比較簡單,根據(jù)config文件中的配置條目,將對應的value值寫入到對應的cgroup文件中。
剩下的事情
主進程陷入等待。子進程開始運行。
總結(jié)
以上是生活随笔為你收集整理的Linux Container 研究报告的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深度优化LNMP之Nginx [2]
- 下一篇: 退出出库复核是什么意思_细思极恐!为什么