日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux Container 研究报告

發布時間:2025/3/15 linux 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux Container 研究报告 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1. 綜述

lxc是Linux Container的用戶態工具包。其代碼由三部分組成:

  • shell腳本,部分lxc命令是用shell腳本寫就的。
  • c語言代碼,最終編譯成可執行文件。這部分代碼也用來提供最終的lxc命令。但是這些代碼以處理命令行參數,讀取配置文件等為主。
  • c語言代碼,最終編譯為動態鏈接庫liblxc.so。該動態庫提供了lxc項目的大部分功能,如配置文件分析、日志記錄、容器的創建、通信等。lxc命令的各項功能基本都是通過調用liblxc.so中的函數來完成的。
  • 在命名習慣上, 生成lxc命令的c源文件都有lxc_的前綴。而生成liblxc.so的c源文件則沒有該前綴。

    2. lxc-start

    lxc-start的執行過程大致就是兩步:

  • 解析命令行和配置文件
  • 創建新的進程運行container
  • 2.1 解析命令行和配置文件

    lxc-start的main函數在lxc_start.c中。從main的流程中能大致窺探出lxc-start的執行過程。首先調用lxc_arguments_parse來分析命令行,再調用lxc_config_read來解析配置文件,等獲取好足夠的信息后,調用lxc_start開始了container。

    lxc_arguments_parse

    函數的實現位于arguments.c中。通過該函數的執行,命令行參數中的信息被存放到了類型為lxc_arguments的變量my_args中。命令行參數中有幾個我們可以注意一下:

    • "-n" 指定了container的名字
    • "-c" 指定了某個文件作為container的console
    • "-s" 在命令行中指定key=value的config選項
    • "-o" 指定輸出的log文件
    • "-l" log的打印級別。用"-l DEBUG"命令行參數,log打印的信息最詳細

    lxc_arguments中有一個字段lxcpath指定了lxc container的存放路徑??梢栽诿顓?P選項中設置。如果不設置,會啟用默認參數。該默認參數是在編譯lxc代碼的時候指定的,一般情況下為"/usr/local/var/lib/lxc"或"/var/lib/lxc"。在本文中,我們一律用LXCPATH來表示該路徑。

    另外,我們約定用CT-NAME表示-n參數指定的container的名稱。

    lxc_config_read

    它的實現在confile.c中。逐行讀取配置文件。并調用confile.c下的parse_line來每行做解析。因為配置文件每行都是key=value的形式的,所以parse_line的主要內容是找到"=",分析出(key, value)對并做處理。

    配置文件的路徑由-f參數指定。不指定則從LXCPATH/CT-NAME/config中讀取。

    confile.c的真正核心內容在與95行定義的一個結構體數組

    static struct lxc_config_t config[] = {{ "lxc.arch", config_personality },{ "lxc.pts", config_pts },{ "lxc.tty", config_tty },{ "lxc.devttydir", config_ttydir },..... };

    其中lxc_config_t結構體的定義如下:

    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結構體的name字段給定了key的值,回調函數cb給出了對應key的處理方法。整個config結構體數組就是鍵值與對應動作的一個查找表。

    現在我們來考慮一個場景。我們在配置文件中寫入了lxc.tty = 4。parse_line會解析出(lxc.tty, 4)的序對。到config中查詢得出其對應的處理函數是config_tty,于是就開始調用config_tty。

    函數指針config_cb一共有三個參數,key指的是如lxc.tty之類的鍵,而value指的是如4之類的值。lxc_conf用來存放config文件的分析結果。

    lxc_config_read讀取好的配置文件放在類型為lxc_conf的結構體指針conf。lxc_conf的定義在conf.h中。

    2.2 調用lxc_start

    讓我們再次回到main函數。前面我們通過分析命令行參數和配置文件,收集了container的一系列信息,接下來就該啟動container了。

    main函數255行的lxc_start打響了了啟動container的第一槍。lxc_start的實現在start.c中,函數原型如下:

    int lxc_start(const char* name, char *const argv[], struct lxc_conf *conf, const char *lxcpath)

    四個參數的含義如下:

    • name?CT-NAME
    • argv container要執行的第一個命令。可以通過命令行參數指定, 如“lxc-start -n android4.2 /init”,這里的argv就會是{"/init", NULL}。如果沒有指定,默認是{"/sbin/init", NULL}
    • conf 前面解析好的container配置文件中指定的配置信息。
    • lxcpath LXCPATH

    lxc_start首先調用lxc_check_inherited來關閉所有打開的文件句柄。0(stdin), 1(stdout), 2(stder)和日志文件除外。緊接著就調__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結構體定義如下:

    struct lxc_operations {int (*start)(struct lxc_handler *, void *);int (*post_start)(struct lxc_handler *, void *); };

    各個參數含義如下:

    • name?CT-NAME
    • conf container配置信息
    • op 用lxc_operator結構體來存放了兩個函數指針start和post_start。這兩個指針分別指向start.c的start函數和post_start函數。
    • data start_args類型的結構體,唯一的成員變量argv指向了lxc_start的實參argv,也就是container要執行的init。
    • lxcpath?LXCPATH

    __lxc_start代碼不復雜。我們將比較重要的幾個函數調用抽出來看。

    lxc_init

    __lxc_start調用的第一個函數,用來初始化lxc_handler結構體。傳入的三個參數依次為:

    • name?CT-NAME
    • conf container的配置信息
    • lxcpath?LXCPATH

    函數先新分配一個lxc_handler的結構體handler,設置其conf、lxcpath和name字段。然后調用了lxc_command_init新創建了一個socket并listen之,新建socket的句柄放置在handler->maincmd_fd中。該socket的作用應為接受外部命令。lxc_command_init的實現在commands.c中。其分析可以詳見模塊commands.c部分。

    接著是lxc_set_state。它將STARTING的狀態消息寫入到另一個socket中。lxc_set_state的實現調用了monitor.c的lxc_moitor_send_state。對其分析可以參見monitor.c模塊。

    接著是部分環境變量的設置:LXC_NAME, LXC_CONFIG_FILE, LXC_ROOTFS_MOUNT, LXC_ROOTFS_PATH, LXC_CONSOLE, LXC_CONSOLE_LOGPATH。

    接下來的四件事情:

  • 調用run_lxc_hooks運行pre-start的腳本。
  • 調用lxc_create_tty創建tty
  • 調用lxc_create_console創建console
  • 調用setup_signal_fd處理進程的信號響應
  • 我們來重點分析第二步和第三步

    終端設備的創建

    1. tty

    lxc_create_tty通過調用openpty的命令來為container分配tty設備。conf->tty參數指定了要分配的tty的個數。conf->tty_info結構體用來存放分配好的tty的相關信息。

    如果conf->tty的值是4,那么lxc_create_tty執行完之后的結果是:

    conf->tty_info->nb_tty: 4 conf->tty_info->pty_info: 大小為4的類型為lxc_pty_info的數組的頭指針

    lxc_pty_info的定義如下:

    struct lxc_pty_info {char name[MAXPATHLEN];int master;int slave;int busy; };

    conf->tty_info->pty_info的每一項都記錄了一個新創建pty的信息,master表示pty master的句柄,slave表示slave的句柄,name表示pty slave的文件路徑,即"/dev/pts/N"。

    2. console

    lxc_create_console同樣調用openpty用來創建console設備。創建好的console設備信息存放在類型為lxc_cosnole的結構體變量conf->console中。

    lxc_console的結構體定義如下:

    struct lxc_console {int slave;int master;int peer;char *path;char *log_path;int log_fd;char name[MAXPATHLEN];struct termios *tios; };

    各個參數的含義如下:

    • slave 新創建pty的slave
    • master 新創建pty的master
    • path console文件路徑,可以通過配置文件"lxc.console.path"或者命令行參數-c指定。默認為"/dev/tty"
    • log_path console的日志路徑
    • peer 打開path, 返回句柄放入peer中。
    • log_fd 打開log_path, 返回句柄放入log_fd中。
    • name slave的路徑。
    • tios 存放tty舊的控制參數。

    lxc_spawn

    讓我們繼續回到__lxc_start的主流程。前面通過調用lxc_init初始化了一個lxc_handler的結構體handler,然后在主流程里,又將傳入的ops參數和data參數賦值給了handler的ops字段和data字段。接著就以handler為參數,調用了lxc_spawn。

    lxc_spawn是啟動新的容器的核心。進程通過Linux系統調用clone創建了擁有自己的PID、IPC、文件系統等獨立的命名空間的新進程。然后在新的進程中執行/sbin/init。接下來我們來看具體過程。

  • 首先調用lxc_sync_init來為將來父子進程同步做初始化。
  • 準備clone調用需要的flag。各個flags如下:
    • CLONE_NEWUTS?子進程指定了新的utsname,即新的“計算機名”
    • CLONE_NEWPID?子進程擁有了新的PID空間,clone出的子進程會變成1號進程
    • CLONE_NEWIPC?子進程位于新的IPC命名空間中。這樣SYSTEM V的IPC對象和POSIX的消息隊列看上去會獨立于原系統。
    • CLONE_NEWNS?子進程會有新的掛載空間。
    • CLONE_NEWNET?如果配置文件中有關于網絡的配置,則會增加該flag。它使得子進程有了新的網絡設備的命名空間
  • 調用pin_rootfs。如果container的根文件系統是一個目錄(而非獨立的塊設備),則在container的根文件系統之外以可寫權限打開一個文件。這樣可以防止container在執行過程中將整個文件系統變成只讀(原因很簡單,因為已經有其他進程以讀寫模式打開一個文件了,所以設備是“可寫忙”的。所以其他進程不能將文件系統重新掛載成只讀)。
  • 調用lxc_clone,在新的命名空間中創建新的進程。
  • 父子進程協同工作,完成container的相關配置。
  • 我們先來看一下lxc_clone是如何創建新的進程的。

    lxc_clone

    該函數的原型如下:

    pid_t lxc_clone(int (*fn)(void *), void *arg, int flags)

    三個參數的含義如下:

    fn: 子進程要執行的函數入口 arg:fn的輸入參數 flags: clone的flags

    lxc_clone為clone api做了一個簡單的封裝,最后結果就是子進程會執行fn(arg)。在lxc_spawn處,lxc_clone是這樣調用的:

    handler->pid = lxc_clone(do_start, handler, handler->clone_flags)

    所以lxc_clone執行完后,handler的pid字段會保留子進程的pid(注意不是“1”,是子進程調用getpid()會變成1)。父進程繼續,子進程執行do_start。

    父子進程同步

    同步機制

    進程間同步的函數實現在sync.c中,其實現機制的分析可以見sync.c模塊。這里只列舉用于同步的函數:

    • lxc_sync_barrier_parent/child(struct lxc_handler* handler, int sequence)?發送sequence給parent/child,同時等待parent/child發送sequence+1的消息過來。
    • lxc_sync_wait_parent/child(struct lxc_handler* handler, int sequence)?等待parent/child發送sequence的消息過來。

    同步完成配置的過程

  • 父進程lxc_clone結束后,開始等待子進程發送LXC_SYNC_CONFIGURE的消息過來。此時,執行do_start的子進程完成了四件事情:
  • 將信號處理表置為正常。
  • 通過prctl api,將子進程設置為“如果父進程退出,則子進程收到SIGKILL的消息”。
  • 關閉不需要的file handler
  • 發送LXC_SYNC_CONFIGURE給父進程,通知父進程可以開始配置。同時等待父進程發送配置完成的消息LXC_SYNC_POST_CONFIGURE
  • 受到子進程發送的LXC_SYNC_CONFIGURE的消息后,父進程繼續執行。父進程執行的動作如下:
  • 調用lxc_cgroup_path_create創建新的cgroup
  • 調用lxc_cgroup_enter將子進程加入到新的cgroup中。
  • 如果有新的網絡命名空間,則調用lxc_assign_network為之分配設備
  • 如果有新的用戶空間,如果配置了用戶ids(包括uid,gid)映射,則做用戶ids映射。該映射將container的id映射到了真正系統中一個不存在的id上,使得container可以在一個虛擬的id空間中做諸如“切換到root”之類的事情。詳細討論可參見模塊相關配置。
  • 發送LXC_SYNC_POST_CONFIGURE給子進程,并等待子進程發送LXC_SYNC_CGROUP的消息
  • 子進程收到LXC_SYNC_POST_CONFIGURE的消息被喚醒。完成如下動作:
  • 如果id已被映射,則切換到root
  • 開始container的設置,調用lxc_setup。主要有utsname、ip、根文件系統、設備掛載、console和tty等終端設備的各個方面的配置。
  • 發送LXC_SYNC_CGROUP給父進程。并等待父進程發送LXC_SYNC_CGROUP_POST消息。
  • 父進程被喚醒。根據配置文件中對CGROUP的相關配置,調用setup_cgroup進行cgroup的設置。然后發送LXC_SYNC_CGROUP_POST給子進程。等待子進程發送LXC_SYNC_CGROUP_POST+1。
  • 子進程被喚醒。調用handler->ops->start函數。實際上是完成了對start.c中start函數的調用。該函數功能簡單,基本就是通過exec執行了container的init程序,默認情況下為/sbin/init.
  • 子進程并沒有給父進程返回LXC_SYNC_CGROUP_POST+1的消息,而是關掉了父子進程間的通信信道。這導致父進程被喚醒。被喚醒后,父進程完成了以下動作:
  • 調用detect_shared_rootfs, 檢測是否共享根文件系統,是的話卸載。
  • 修改子進程tty文件的用戶ids
  • 執行handler->ops->post_start, 打印"XXX is started with pid XXX"字樣。
  • 相關配置

    在這一部分中,我們針對前面講的父子進程的同步配置過程來對部分重要的函數做分析。

    lxc_cgroup_path_create和lxc_cgroup_enter

    函數定義在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函數的作用是在cgroup各個已掛載使用的子系統的掛載點上為新創建的container新建一個文件夾。lxc_cgroup_enter的作用是把新創建的container加入到group cgpath中。

    下面我們來舉例說明: 比如掛載的子系統有blkio和cpuset,他們的掛載點分別是/cgroup/blkio和/cgroup/cpuset。

    lxc_cgroup_path_create函數運行結束后,則會多出兩個目錄/cgroup/blkio/lxcgroup/name和/cgroup/cpuset/lxcgroup/name。如果傳入參數lxcgroup為空,則會使用“lxc”。函數的返回值是新創建目錄的相對路徑。即“lxcgroup/name”。

    lxc_cgroup_enter函數結束后,進程號"pid"會被追加到文件/cgroup/blkio/lxcgroup/name/tasks和/cgrop/cpuset/lxcgroup/name/tasks中。在lxc_spawn中,pid的實參用的是handler->pid,即clone出的子進程的id。

    通過查看/proc/mounts查看cgroup的掛載點。通過查看/proc/cgroups查看正掛載使用的子系統。

    id映射

    查看confile.c下的config_idmap函數,可知在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)會被映射到真實系統中的 [id_outside_ns, id_outside_ns+range)。

    在config_idmap執行后,配置文件中的配置條目被作為鏈表存放到conf->id_map字段下。在lxc_spawn中,通過調用lxc_map_ids函數來完成配置。

    lxc_map_ids的實現在conf.c中,原型如下:

    int lxc_map_ids(struct lxc_list *idmap, pid_t pid)

    第一個參數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中調用的lxc_setup是一個非常重要的函數。container里面的很多配置都是在lxc_setup中完成的。這里重點分析setup_console和setup_tty兩個函數,來查看比較困擾的終端字符設備是如何虛擬的。

    setup_console的實現在conf.c中,函數原型如下:

    int setup_console(const struct lxc_rootfs *rootfs, const struct lxc_console *console, char *ttydir)

    rootf變量描述了container根文件系統的路徑和掛載點。console變量描述了container的console設備的相關信息。在do_start的調用中,傳來的實參是lxc_conf->console,這個字段是我們在lxc_init時初始化的。回顧當時的初始化過程:

    • console->slave和console->master分別存儲了新分配的pty的slave和master的句柄。
    • console->peer存儲了打開原系統"/dev/tty"的句柄。
    • console->name指向了新分配pty的文件路徑

    setup_console根據ttydir的值,分支調用了setup_dev_console或者setup_ttydir_console。我們只看setup_dev_console來了解原理。

    在setup_dev_console中,主要動作就是將console->name指向的pty通過BIND的方式掛載到rootfs/dev/console文件中。可以看出,我們對container /dev/console的訪問,實質上是對新分配pty的訪問。

    setup_tty的過程與此類似,在rootfs/dev/目錄下創建tty1, tty2等常規文件,然后用BIND的方式將新創建的pty掛載到其上。

    setup_cgroup

    顯然,container無法訪問原來的cgroup根文件系統,所以這個任務只能由父進程在lxc_spawn中調用。該函數實現比較簡單,根據config文件中的配置條目,將對應的value值寫入到對應的cgroup文件中。

    剩下的事情

    主進程陷入等待。子進程開始運行。

    總結

    以上是生活随笔為你收集整理的Linux Container 研究报告的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。