這篇文章給大家分享的是有關docker中run的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
通過在/components/cli/command/commands.go里,抽象出各種命令的初始化操作。
使用第三方庫"github.com/spf13/cobra"
docker run 初始化命令行終端解析參數,最終生成 APIclient發出REQUEST請求給docker daemon.
docker daemon的初始化,設置了server的監聽地址,初始化化routerSwapper, registryService 以及layStore、imageStore、volumeStore等各種存儲 。
docker run的命令解析為 docker container create 和 container start 兩次請求:
其中container create 不涉及底層containerd的調用,首先將host.config 、networkingConfig和AdjustCPUShares等組裝成一個客戶端請求,發送到docker daemon注冊該容器。該請求會完成拉取image, 以及初始化 baseContainer的RWlayer, config文件等,之后daemon就可以通過containerid來使用該容器。
container start 命令的核心是調用了daemon的containerStart(),它會完成
調用containerd進行create容器,調用libcontainerd模塊 clnt *client 的初始化,
設置容器文件系統,掛載點: /var/lib/docker/overlay/{container.RWLayer.mountID}/merged
設置容器的網絡模式,調用libnetwork ,CNM模型(sandbox, endpoint,network)
創建/proc /dev等spec文件,對容器所特有的屬性進行設置,
調用containerd進行create容器
1)獲取libcontainerd模塊中的containers 2)獲取gid和uid 3)創建state目錄,配置文件路徑。 4)創建一個containercommon對象,創建容器目錄,以及配置文件路徑,根據spec創建配置文件。
1) 讀取spec對象的配置文件 2) 創建一個fifo的pipe 3) 定義containerd的請求對象,grpc調用containerd模塊。 ctr.client.remote.apiClient.CreateContainer(context.Background(), r) 4)啟動成功后,更新容器狀態。
cmd/dockerd/daemon.go 中存在libcontainerd初始化的流程。
包括啟動grpc服務器,對套接字進行監聽。
通過grpc.dail 與grpc server建立連接conn, 根據該鏈接建立apiclient對象,發送json請求。
runContainerdDaemon
通過docker-containerd二進制與grpc server進行通信,
docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc
執行結果的輸入輸出流重定向到docker daemon。
runc把state.json文件保存在容器運行時的狀態信息,默認存放在/run/runc/{containerID}/state.json。type Supervisor struct {
// stateDir is the directory on the system to store container runtime state information.
stateDir string
// name of the OCI compatible runtime used to execute containers
runtime string
runtimeArgs []string
shim string
containers map[string]*containerInfo
startTasks chan *startTask //這是containerd到runc的橋梁,由func (w *worker) Start()消費
// we need a lock around the subscribers map only because additions and deletions from
// the map are via the API so we cannot really control the concurrency
subscriberLock sync.RWMutex
subscribers map[chan Event]struct{}
machine Machine
tasks chan Task //所有來自于docker-daemon的request都會轉化為event存放到這,由func (s *Supervisor) Start()消費
monitor *Monitor
eventLog []Event
eventLock sync.Mutex
timeout time.Duration
}
type startTask struct {
Container runtime.Container
CheckpointPath string
Stdin string
Stdout string
Stderr string
Err chan error
StartResponse chan StartResponse
}我們知道containerd作為docker daemon的grpc server端,通過接收 apiclient request轉化成對應的events,在不同的子系統distribution , bundles , runtime 中進行數據的流轉,包括鏡像上傳下載,鏡像打包和解壓,運行時的創建銷毀等。
其中containerd 核心組件包括 supervisor 和executor, 數據流如下:
docker-daemon --->tasks chan Task --->func (s *Supervisor) Start()消費 --->存放到startTasks chan *startTask -->func (w *worker) Start()消費
docker-containerd初始化包括 新建Supervisor對象:
該對象會啟動10個worker,負責處理創建新容器的任務(task)。
supervisor的初始化,包括startTask chan初始化,啟動監控容器進程的monitor
一個worker包含一個supervisor和sync.waitgroup,wg用于實現容器啟動。
supervisor的start,消費tasks,把task中的container數據組裝成runtime.container, 封裝到type startTask struct,發送到startTask chan隊列。
啟動grpc server(startServer),用來接收dockerd的request請求。
func daemon(context *cli.Context) error {
s := make(chan os.Signal, 2048)
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
/*
新建一個supervisor,這個是containerd的核心部件
==>/supervisor/supervisor.go
==>func New
*/
sv, err := supervisor.New(
context.String("state-dir"),
context.String("runtime"),
context.String("shim"),
context.StringSlice("runtime-args"),
context.Duration("start-timeout"),
context.Int("retain-count"))
if err != nil {
return err
}
wg := &sync.WaitGroup{}
/*
supervisor 啟動10個worker
==>/supervisor/worker.go
*/
for i := 0; i < 10; i++ {
wg.Add(1)
w := supervisor.NewWorker(sv, wg)
go w.Start()
}
//啟動supervisor
if err := sv.Start(); err != nil {
return err
}
// Split the listen string of the form proto://addr
/*
根據參數獲取監聽器
listenSpec的值為 unix:///var/run/docker/libcontainerd/docker-containerd.sock
*/
listenSpec := context.String("listen")
listenParts := strings.SplitN(listenSpec, "://", 2)
if len(listenParts) != 2 {
return fmt.Errorf("bad listen address format %s, expected proto://address", listenSpec)
}
/*
啟動grpc server端
*/
server, err := startServer(listenParts[0], listenParts[1], sv)
if err != nil {
return err
}其中startServer負責啟動grpc server,監聽docker-containerd.sock,聲明注冊路由handler。
當CreateContainer handler接收到一個Request之后,會把其轉化成type startTask struct,將其轉化為一個StartTask 事件,其中存放創建容器的request信息。
通過s.sv.SendTask(e)將該事件發送給supervosior 主循環。
// SendTask sends the provided event to the the supervisors main event loop
/*
SendTask將evt Task發送給 the supervisors main event loop
所有來自于docker-daemon的request都會轉化為event存放到這,生產者
*/
func (s *Supervisor) SendTask(evt Task) {
TasksCounter.Inc(1) //任務數+1
s.tasks <- evt
}等待woker.Start()消費處理結果后,將StartResponse返回給docker-daemon。
負責將每一個request轉化成特定的task類型,通過一個goroutine遍歷task中所有的任務并進行處理。消費tasks,把task中的container數據組裝成runtime.container, 封裝到type startTask struct,發送到startTask chan隊列。
負責調用containerd-shim, 監控容器中的進程,并把結果返回給StartResponse chan隊列。
其中,
container.Start() 通過containerd-shim 調用runc create {containerID}創建容器。
process, err := t.Container.Start(t.CheckpointPath, runtime.NewStdio(t.Stdin, t.Stdout, t.Stderr)) 其中值得注意的是,container.start 和container.exec均是調用createcmd,exec 命令則是通過process.json中的相關屬性來判斷是Start()還是Exec(),最后組裝成containerd-shim的調用命令。 當具體容器內進程pid生成(由runc生成)后,createCmd會啟動一個go routine來等待shim命令的結束。 shim命令一般不會退出。 當shim發生退出時,如果容器內的進程仍在運行,則需要把該進程殺死;如果容器內進程已經不存在,則無需清理工作。
process.Start() 通過調用runc start {containerID}命令啟動容器的init進程
root@idc-gz:/var/run/docker/libcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/ eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/ ├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdin ├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdout ├── config.json ├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdin ├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdout ├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdin ├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdout ├── init-stdin └── init-stdout root@idc-gz:/var/run/docker/libcontainerdcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/ eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/ ├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5 │ ├── control │ ├── exit │ ├── log.json │ ├── pid │ ├── process.json │ ├── shim-log.json │ └── starttime ├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844 │ ├── control │ ├── exit │ ├── log.json │ ├── pid │ ├── process.json │ ├── shim-log.json │ └── starttime ├── init │ ├── control │ ├── exit │ ├── log.json │ ├── pid │ ├── process.json │ ├── shim-log.json │ └── starttime └── state.json
在源碼create.go中,首先會加載config.json的配置,然后調用startContainer函數,其流程包括:
createContainer, 生成libcontainer.Container對象,狀態處于stopped、destoryed。
調用loadFactory方法, 生成一個libcontainer.Factory對象。
調用factory.Create()方法,生成libcontainer.Container
把libcontainer.Container封裝到type runner struct對象中。
runner.run負責將config.json設置將來在容器中啟動的process,設置iopipe和tty
runc create ,調用container.Start(process)
linuxContainer.newParentPorcess組裝要執行的parent命令, 組裝出來的命令是/proc/self/exe init, 通過匿名管道讓runc create 和runc init進行通信。
parent.start()會根據parent的類型來選擇對應的start(),自此之后,將進入/proc/self/exe init,也就是runc init
將容器狀態持久化到state.json,此時容器狀態為created.
runc start,調用container.Run(process)
// LinuxFactory implements the default factory interface for linux based systems.
type LinuxFactory struct {
// Root directory for the factory to store state.
/*
factory 存放數據的根目錄 默認是 /run/runc
而/run/runc/{containerID} 目錄下,會有兩個文件:
一個是管道exec.fifo
一個是state.json
*/
Root string
// InitArgs are arguments for calling the init responsibilities for spawning
// a container.
/*
用于設置 init命令 ,固定是 InitArgs: []string{"/proc/self/exe", "init"},
*/
InitArgs []string
// CriuPath is the path to the criu binary used for checkpoint and restore of
// containers.
// 用于checkpoint and restore
CriuPath string
// Validator provides validation to container configurations.
Validator validate.Validator
// NewCgroupsManager returns an initialized cgroups manager for a single container.
// 初始化一個針對單個容器的cgroups manager
NewCgroupsManager func(config *configs.Cgroup, paths map[string]string) cgroups.Manager
}
// 一個容器負責對應一個runner
type runner struct {
enableSubreaper bool
shouldDestroy bool
detach bool
listenFDs []*os.File
pidFile string
console string
container libcontainer.Container
create bool
}runc create clone出一個子進程,namespace與父進程隔離,子進程中調用/proc/self/exe init進行初始化。
runc init的過程如下:
調用factory.StartInitialization();
配置容器內部網絡,路由,初始化mount namespace, 調用setupRootfs在新的mount namespaces中配置設備、掛載點以及文件系統。
配置hostname, apparmor,processLabel,sysctl, readyonlyPath, maskPath.
獲取父進程的退出信號,通過管道與父進程同步,先發出procReady再等待procRun
恢復parent進程的death信號量并檢查當前父進程pid是否為我們原來記錄的不是的話,kill ourself。
與父進程之間的同步已經完成,關閉pipe。
"只寫" 方式打開fifo管道并寫入0,會一直保持阻塞。等待runc start以只讀的方式打開FIFO管道,阻塞才會消除。之后本進程才會繼續執行。
調用syscall.Exec,執行用戶真正希望執行的命令。用來覆蓋掉PID為1的Init進程。至此,在容器內部PID為1的進程才是用戶希望一直在前臺執行的進程。
init進程通過匿名管理讀取父進程的信息,initType以及config信息。
調用func newContainerInit(),生成一個type linuxStandardInit struct對象
執行linuxStandardInit.Init(),Init進程會根據config配置初始化seccomp,并調用syscall.Exec執行cmd。
runc start的邏輯比較簡單,分為兩步:
從context中獲取libcontainer.container對象。
通過判斷container 的狀態為created,執行linuxContainer.exec()。
以“只讀”的方式打開FIFO管道,讀取內容。這同時也恢復之前處于阻塞狀態的`runc Init`進程,Init進程會執行最后調用用戶期待的cmd部分。
如果讀取到的data長度大于0,則讀取到Create流程中最后寫入的“0”,則刪除FIFO管道文件。
感謝各位的閱讀!關于“docker中run的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。