在kubernetes的實(shí)際生產(chǎn)實(shí)踐中,經(jīng)常會(huì)看到pod內(nèi)的容器因?yàn)閮?nèi)存使用超限被內(nèi)核kill掉劝篷,使用kubectl命令查看pod枫笛,可以看到容器的退出原因是OOMKilled法精,退出碼是137徒河。
文章導(dǎo)讀
cgroup簡介與使用
linux epoll原理分析
containerd代碼解析
kubelet代碼解析
使用event_control監(jiān)聽oom事件
經(jīng)過前面幾篇文章的鋪墊與遞進(jìn)系馆,本篇終于可以進(jìn)入正題,已以下兩條主線進(jìn)行分析顽照。
- 容器的退出原因OOMKilled是如何經(jīng)由containerd更新到kubelet它呀,并最終更新到Pod的status中。
- 退出碼為何是137棒厘。
本文的分析思路是由下至上,先分析內(nèi)核的oom是如何觸發(fā)的下隧,再分析進(jìn)程被kill掉后奢人,退出碼是何時(shí)賦值的,最后再分析containerd-shim淆院,containerd何乎,kubelet是如何處理進(jìn)程的oom狀態(tài)和退出碼。
場景再現(xiàn)
本次實(shí)驗(yàn)基于4.19內(nèi)核的centos的系統(tǒng)土辩,kubernetes版本為1.23.1支救,kubelet的運(yùn)行時(shí)配置為containerd,containerd版本為1.4拷淘,cgroup使用v1版本各墨。
首先,編輯一個(gè)Pod的yaml文件启涯,配置Pod的restartPolicy為Nerver贬堵,內(nèi)存限額設(shè)置為50M,容器鏡像為mem_alloc:v1结洼。該容器啟動(dòng)后會(huì)不斷申請并使用內(nèi)存黎做,直到內(nèi)存超限,被內(nèi)核kill掉松忍。
[root@localhost oom]# cat pod_oom.yaml
apiVersion: v1
kind: Pod
metadata:
name: lugl-oom-test
spec:
restartPolicy: Never
containers:
- name: oom-test
image: docker.io/registry/mem_alloc:v1
imagePullPolicy: IfNotPresent
resources:
limits:
memory: "50Mi"
使用kubectl create -f pod_oom.yaml創(chuàng)建該pod蒸殿,過一會(huì)兒使用kubectl查看pod的狀態(tài)(刪除掉不相關(guān)的字段信息)。可以看到容器的退出原因(reason)為OOMKilled宏所,退出碼(exitCode)為137酥艳。其中還有個(gè)字段lastState可以記錄上一次的容器的狀態(tài)信息,這里為了簡化分析楣铁,忽略該字段玖雁。
[root@localhost oom]# kubectl get pods lugl-oom-test -o yaml
apiVersion: v1
kind: Pod
spec:
containers:
- image: docker.io/registry/mem_alloc:v1
imagePullPolicy: IfNotPresent
name: oom-test
resources:
limits:
memory: 50Mi
requests:
memory: 50Mi
restartPolicy: Never
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2022-02-20T02:10:30Z"
status: "True"
type: Initialized
containerStatuses:
- containerID: containerd://388f01cfb6b8ba2817ff85ef8e72c654f866200d96123a623a73e0304e4934cf
image: docker.io/registry/mem_alloc:v1
imageID: sha256:3bd194272e76fa429b1c5df19d91bcf47eacc1ed07e3afe378ec3d7b49524ef0
lastState: {}
name: oom-test
ready: false
restartCount: 0
started: false
state:
terminated:
containerID: containerd://388f01cfb6b8ba2817ff85ef8e72c654f866200d96123a623a73e0304e4934cf
exitCode: 137
finishedAt: "2022-02-20T02:10:42Z"
reason: OOMKilled
startedAt: "2022-02-20T02:10:31Z"
oom機(jī)制和信號(hào)處理
(1) 本環(huán)境中的PAGESIZE為4K,mem_alloc程序在運(yùn)行中盖腕,會(huì)申請內(nèi)存并使用赫冬,所以會(huì)不斷觸發(fā)缺頁異常,內(nèi)核在每次申請內(nèi)存頁時(shí)都會(huì)去比較該cgroup進(jìn)程的內(nèi)存使用量 是否超過了該cgroup內(nèi)設(shè)置的閾值(memory.limit_in_bytes)溃列,如果超過閾值劲厌,并且在該cgroup內(nèi)也沒法通過內(nèi)存回收釋放足夠的內(nèi)存,則內(nèi)核會(huì)在該cgroup內(nèi)選擇一個(gè)進(jìn)程kill掉(oom_badness)听隐。
(2)當(dāng)觸發(fā)oom時(shí)补鼻,內(nèi)核會(huì)向進(jìn)程發(fā)送SIGKILL信號(hào),這里的發(fā)送信號(hào)是擬人化的寫法雅任,方便人去理解风范,但實(shí)際的內(nèi)核發(fā)送信號(hào)僅僅是對進(jìn)程數(shù)據(jù)結(jié)構(gòu)(task_struct)中的信號(hào)相關(guān)字段的修改,如將SIGKILL這個(gè)sig添加到該進(jìn)程的信號(hào)處理隊(duì)列中沪么,這樣就算內(nèi)核完成了信號(hào)的發(fā)送硼婿。
SIGKILL(對應(yīng)信號(hào)值9)和SIGSTOP(對應(yīng)信號(hào)值19)是兩個(gè)特權(quán)信號(hào),即用戶不可以注冊這兩個(gè)信號(hào)的信號(hào)處理函數(shù)禽车,由內(nèi)核的默認(rèn)函數(shù)進(jìn)行處理寇漫。 SIGTERM(對應(yīng)信號(hào)值15)是可以被注冊信號(hào)處理函數(shù)的。kill命令不加參數(shù)默認(rèn)就是向進(jìn)程發(fā)送SIGTERM信號(hào)殉摔。kubelet停止pod時(shí)州胳,也是先發(fā)送SIGTERM信號(hào),經(jīng)過terminationGracePeriodSeconds時(shí)間后逸月,如果pod沒有退出再發(fā)送SIGKILL信號(hào)栓撞。所以容器內(nèi)的進(jìn)程最好是注冊SIGTERM對應(yīng)的信號(hào)處理函數(shù),在進(jìn)程退出的時(shí)候做一些資源清理的操作碗硬,減少異常的發(fā)生腐缤,如關(guān)閉遠(yuǎn)端連接。容器內(nèi)的init進(jìn)程退出時(shí)肛响,內(nèi)核會(huì)向該init進(jìn)程命名空間下的其他進(jìn)程發(fā)送SIGKILL信號(hào)岭粤,更完善的做法是init進(jìn)程攔截SIGTERM信號(hào)后,將SIGTERM轉(zhuǎn)發(fā)給子進(jìn)程特笋,這樣init和init的子進(jìn)程都可以實(shí)現(xiàn)優(yōu)雅退出剃浇,如docker的 --init參數(shù)就是使用一個(gè)專用的init進(jìn)程接管用戶的程序巾兆。
下面就是mem_alloc進(jìn)程因?yàn)閛om被kil掉的堆棧信息。
# 設(shè)置以下關(guān)注函數(shù)
[root@node-135 tracing]# cat set_ftrace_filter
__send_signal
do_send_sig_info
__oom_kill_process
out_of_memory
mem_cgroup_out_of_memory
try_charge
# cat trace_pipe
=> __send_signal -- 向進(jìn)程發(fā)送SIGKILL信號(hào)
=> do_send_sig_info
=> __oom_kill_process -- kill掉一些進(jìn)程
=> oom_kill_process
=> out_of_memory -- 內(nèi)存不足
=> mem_cgroup_out_of_memory
=> try_charge
=> mem_cgroup_try_charge -- cgroup內(nèi)的內(nèi)存是否充足
=> mem_cgroup_try_charge_delay
=> __handle_mm_fault
=> handle_mm_fault
=> __do_page_fault
=> do_page_fault
=> page_fault -- 缺頁異常
(3)內(nèi)核修改了進(jìn)程的task_struct虎囚,表明該進(jìn)程有信號(hào)需要處理角塑。用戶注冊的信號(hào)處理函數(shù)在用戶態(tài),此時(shí)還在內(nèi)核中執(zhí)行異常處理流程淘讥,那么信號(hào)處理函數(shù)的執(zhí)行時(shí)機(jī)是什么呢圃伶? 在返回到用戶態(tài)的時(shí)候是一個(gè)執(zhí)行時(shí)機(jī)。信號(hào)處理的實(shí)現(xiàn)比較復(fù)雜蒲列,具體細(xì)節(jié)分析可以參見極客時(shí)間《趣談linux操作系統(tǒng)》中的信號(hào)處理章節(jié)窒朋,詳細(xì)介紹了信號(hào)處理函數(shù)執(zhí)行的時(shí)機(jī),以及執(zhí)行完信號(hào)處理函數(shù)又是如何返回到正常的執(zhí)行流程中的整個(gè)過程蝗岖。
以下堆棧信息展示了mem_alloc程序的退出過程侥猩。
# 以下的函數(shù)堆棧是一個(gè)SIGKILL信號(hào)的處理
=> __send_signal -- 向父進(jìn)程發(fā)送SIGCHILD信號(hào)
=> do_notify_parent
=> do_exit -- 在這個(gè)函數(shù)里會(huì)設(shè)置進(jìn)程的退出碼
=> do_group_exit -- 這個(gè)函數(shù)的參數(shù)只有一個(gè)退出碼
=> get_signal -- 獲取需要處理的信號(hào)
=> do_signal
=> exit_to_usermode_loop -- 返回到用戶態(tài)
=> prepare_exit_to_usermode
=> swapgs_restore_regs_and_return_to_usermode
(4)正常的程序在退出時(shí)是調(diào)用exit系統(tǒng)調(diào)用,exit是調(diào)用do_exit函數(shù)抵赢,在(3)的堆棧中可以看到處理SIGKILL也會(huì)調(diào)用do_exit欺劳,區(qū)別在于正常的退出,錯(cuò)誤碼是經(jīng)過了處理的铅鲤,而在處理SIGKILL信號(hào)時(shí)划提,是直接將信號(hào)的值賦值給了進(jìn)程的退出碼(task_struct.exit_code)。
SYSCALL_DEFINE1(exit, int, error_code)
{
do_exit((error_code&0xff)<<8);
}
(5)使用$?查看程序的退出碼邢享,這也上面的分析不一致鹏往,錯(cuò)誤碼應(yīng)該是9,還不是137驼仪,問題在哪里呢?
[root@localhost test]# echo 104857600 > memory.limit_in_bytes
[root@localhost test]# echo $$ > cgroup.procs
[root@localhost test]# /root/training/memory/oom/mem-alloc/mem_alloc 40000
Allocating,set to 40000 Mbytes
Killed
[root@localhost test]# echo $?
137
(6)重復(fù)(5)中的步驟袜漩,使用ftrace查看函數(shù)do_exit的參數(shù)值绪爸。從下面的trace_pipe中確實(shí)看到mem_alloc的退出碼確實(shí)是9,而不是137宙攻。這里直接說結(jié)論奠货,在下面會(huì)再次驗(yàn)證,bash進(jìn)程是mem_alloc的父進(jìn)程座掘,在處理mem_alloc進(jìn)程的返回值時(shí)递惋,如果是9,說明是被SIGKILL掉的溢陪,所以將9加上一個(gè)偏移量128萍虽,結(jié)果就是137,這樣做可能是為了與linux系統(tǒng)標(biāo)準(zhǔn)的錯(cuò)誤碼做一個(gè)區(qū)分吧形真。
# 設(shè)置kprobe
[root@localhost tracing]# echo 'p:myprobe do_exit exit_code=%di' > kprobe_events
[root@localhost tracing]# echo 1 > events/kprobes/myprobe/enable
# 設(shè)置過濾條件
[root@localhost tracing]# echo exit_code==9 > events/kprobes/myprobe/filter
[root@localhost tracing]# /root/training/memory/oom/mem-alloc/mem_alloc 40000
Allocating,set to 40000 Mbytes
Killed
#查看結(jié)果
[root@localhost tracing]# cat trace_pipe
mem_alloc-17077 [001] .... 875.147095: myprobe: (do_exit+0x0/0xc60) exit_code=0x9
containerd-shim 進(jìn)程的處理
containerd-shim監(jiān)聽cgroup內(nèi)進(jìn)程的oom事件與使用event_control監(jiān)聽oom事件中的一致杉编,這里不再贅述。
(1) 在Run函數(shù)中會(huì)監(jiān)聽eventfd,如果epoll_wait返回邓馒,則說明該cgroup內(nèi)有進(jìn)程觸發(fā)了oom嘶朱。
func (e *epoller) Run(ctx context.Context) {
var events [128]unix.EpollEvent
for {
select {
default:
n, err := unix.EpollWait(e.fd, events[:], -1)
for i := 0; i < n; i++ {
e.process(ctx, uintptr(events[i].Fd))
}
}
}
}
(2) process函數(shù)中會(huì)通過GRPC消息將TaskOOMEvent轉(zhuǎn)發(fā)到containerd中。
func (e *epoller) process(ctx context.Context, fd uintptr) {
if err := e.publisher.Publish(ctx, runtime.TaskOOMEventTopic, &eventstypes.TaskOOM{
ContainerID: i.id,
});
}
(3)在進(jìn)程退出時(shí)光酣,內(nèi)核會(huì)向其父進(jìn)程(containerd-shim)發(fā)送SIGCHILD信號(hào)疏遏。handleSignals會(huì)處理SIGCHILD信號(hào),調(diào)用Reap救军,進(jìn)而使用系統(tǒng)調(diào)用wait4拿到進(jìn)程的退出碼财异。
// runtime/v2/shim/shim_unix.go
func handleSignals(ctx context.Context, logger *logrus.Entry, signals chan os.Signal) error {
for {
select {
case s := <-signals:
switch s {
case unix.SIGCHLD:
if err := reaper.Reap(); err != nil {
logger.WithError(err).Error("reap exit status")
}
}
}
}
(4)在containerd-shim啟動(dòng)的時(shí)候,會(huì)啟動(dòng)goroutine監(jiān)聽容器進(jìn)程的退出缤言,checkProcess中會(huì)將容器id宝当,進(jìn)程id,退出碼胆萧,退出時(shí)間等信息通過GRPC消息轉(zhuǎn)發(fā)到containerd中庆揩。
func New(ctx context.Context, id string, publisher shim.Publisher, shutdown func()) (shim.Shim, error) {
if cgroups.Mode() == cgroups.Unified {
ep, err = oomv2.New(publisher)
} else {
ep, err = oomv1.New(publisher)
}
go ep.Run(ctx)
go s.processExits()
go s.forward(ctx, publisher)
}
func (s *service) checkProcesses(e runcC.Exit) {
for _, container := range s.containers {
for _, p := range container.All() {
p.SetExited(e.Status)
s.sendL(&eventstypes.TaskExit{
ContainerID: container.ID,
ID: p.ID(),
Pid: uint32(e.Pid),
ExitStatus: uint32(e.Status),
ExitedAt: p.ExitedAt(),
})
}
}
}
(5)回收子進(jìn)程會(huì)調(diào)用reap函數(shù),在exitStatus中跌穗,會(huì)判斷進(jìn)程是否是被信號(hào)中斷的订晌,如果是的話,則加上一個(gè)偏移量128蚌吸,這里就驗(yàn)證了上面說的結(jié)論锈拨,137 = 9 + 128。那么可以推斷bash中 $羹唠?也是同樣的處理奕枢。
func reap(wait bool) (exits []exit, err error) {
for {
pid, err := unix.Wait4(-1, &ws, flag, &rus)
exits = append(exits, exit{
Pid: pid,
Status: exitStatus(ws),
})
}
}
// sys/reaper/reaper_unix.go
const exitSignalOffset = 128
func exitStatus(status unix.WaitStatus) int {
if status.Signaled() {
return exitSignalOffset + int(status.Signal())
}
return status.ExitStatus()
}
// unix/syscall_linux.go
const (
mask = 0x7F
core = 0x80
exited = 0x00
stopped = 0x7F
shift = 8
)
func (w WaitStatus) Signaled() bool {
return w&mask != stopped && w&mask != exited
}
containerd的處理
(1)在handleEvent中會(huì)處理containerd-shim轉(zhuǎn)發(fā)過來的容器的事件信息,如果是TaskOOM佩微,則調(diào)用UpdateSync更新容器的退出原因缝彬。
(2)如果是進(jìn)程退出,則調(diào)用handleContainerExit哺眯,再調(diào)用UpdateSync更新容器的退出碼谷浅。
// handleEvent handles a containerd event.
func (em *eventMonitor) handleEvent(any interface{}) error {
switch e := any.(type) {
case *eventtypes.TaskExit:
logrus.Infof("TaskExit event %+v", e)
cntr, err := em.c.containerStore.Get(e.ID)
handleContainerExit(ctx, e, cntr);
case *eventtypes.TaskOOM:
logrus.Infof("TaskOOM event %+v", e)
// For TaskOOM, we only care which container it belongs to.
cntr, err := em.c.containerStore.Get(e.ContainerID)
err = cntr.Status.UpdateSync(func(status containerstore.Status) (containerstore.Status, error) {
status.Reason = oomExitReason
return status, nil
})
return nil
}
func handleContainerExit(ctx context.Context, e *eventtypes.TaskExit, cntr containerstore.Container) error {
err = cntr.Status.UpdateSync(func(status containerstore.Status) (containerstore.Status, error) {
if status.FinishedAt == 0 {
status.Pid = 0
status.FinishedAt = e.ExitedAt.UnixNano()
status.ExitCode = int32(e.ExitStatus)
}
return status, nil
})
}
(3)UpdateSync是containerd中用來更新容器存儲(chǔ)狀態(tài)的函數(shù)。
func (s *statusStorage) UpdateSync(u UpdateFunc) error {
newStatus, err := u(s.status)
data, err := newStatus.encode()
continuity.AtomicWriteFile(s.path, data, 0600)
s.status = newStatus
}
(4)containerd的處理就算完成了奶卓,接下來就等kubelet來獲取容器的狀態(tài)了一疯。
kubelet的處理
kubelet的細(xì)節(jié)分析參見 kubelet代碼解析
(1)relist函數(shù)會(huì)定期執(zhí)行,對比pod狀態(tài)的變化夺姑。
(2)通過computeEvent墩邀、updateEvents和generateEvents對比內(nèi)存中的pod的狀態(tài)和從runtime接口獲取的實(shí)時(shí)的pod的狀態(tài)差異,從而可以推斷出pod內(nèi)的容器發(fā)生了狀態(tài)變化盏浙。
func (g *GenericPLEG) Start() {
go wait.Until(g.relist, g.relistPeriod, wait.NeverStop)
}
func (g *GenericPLEG) relist() {
klog.V(5).InfoS("GenericPLEG: Relisting")
for pid := range g.podRecords {
oldPod := g.podRecords.getOld(pid)
pod := g.podRecords.getCurrent(pid)
// Get all containers in the old and the new pod.
allContainers := getContainersFromPods(oldPod, pod)
for _, container := range allContainers {
events := computeEvents(oldPod, pod, &container.ID)
for _, e := range events {
updateEvents(eventsByPodID, e)
}
}
}
}
(3)如因oom產(chǎn)生一個(gè)類型為ContainerDied的PodLifecycleEvent事件
func generateEvents(podID types.UID, cid string, oldState, newState plegContainerState) []*PodLifecycleEvent {
klog.V(4).InfoS("GenericPLEG", "podUID", podID, "containerID", cid, "oldState", oldState, "newState", newState)
switch newState {
case plegContainerRunning:
return []*PodLifecycleEvent{{ID: podID, Type: ContainerStarted, Data: cid}}
case plegContainerExited:
return []*PodLifecycleEvent{{ID: podID, Type: ContainerDied, Data: cid}}
case plegContainerUnknown:
return []*PodLifecycleEvent{{ID: podID, Type: ContainerChanged, Data: cid}}
}
(4)在syncLoopIteration中會(huì)處理該pleg事件磕蒲,繼而調(diào)用Podworker的sync方法留潦。
func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate, handler SyncHandler,
syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool {
select {
case e := <-plegCh:
handler.HandlePodSyncs([]*v1.Pod{pod})
}
(5)generateAPIPodStatus會(huì)將容器的狀態(tài)轉(zhuǎn)換為 v1.PodStatus定義中的字段。再調(diào)用statusManager的SetPodStatus方法更新pod的狀態(tài)信息辣往。
func (kl *Kubelet) syncPod(ctx context.Context, updateType kubetypes.SyncPodType, pod, mirrorPod *v1.Pod, podStatus *kubecontainer.PodStatus) error {
klog.V(4).InfoS("syncPod enter", "pod", klog.KObj(pod), "podUID", pod.UID)
// Generate final API pod status with pod and status manager status
apiPodStatus := kl.generateAPIPodStatus(pod, podStatus)
kl.statusManager.SetPodStatus(pod, apiPodStatus)
// Call the container runtime's SyncPod callback
result := kl.containerRuntime.SyncPod(pod, podStatus, pullSecrets, kl.backOff)
}
(6)根據(jù)狀態(tài)做不同的賦值兔院,如ContainerStateExited,則要更新容器的退出碼站削,原因等坊萝。
func (kl *Kubelet) convertToAPIContainerStatuses(pod *v1.Pod, podStatus *kubecontainer.PodStatus, previousStatus []v1.ContainerStatus, containers []v1.Container, hasInitContainers, isInitContainer bool) []v1.ContainerStatus {
switch {
case cs.State == kubecontainer.ContainerStateRunning:
status.State.Running = &v1.ContainerStateRunning{StartedAt: metav1.NewTime(cs.StartedAt)}
case cs.State == kubecontainer.ContainerStateCreated:
fallthrough
case cs.State == kubecontainer.ContainerStateExited:
status.State.Terminated = &v1.ContainerStateTerminated{
ExitCode: int32(cs.ExitCode),
Reason: cs.Reason,
Message: cs.Message,
StartedAt: metav1.NewTime(cs.StartedAt),
FinishedAt: metav1.NewTime(cs.FinishedAt),
ContainerID: cid,
}
}
(7)getPhase也是一個(gè)比較重要的函數(shù),這里會(huì)決定pod的狀態(tài)许起,PodSpec中的restartPolicy是在這里生效的十偶。
func getPhase(spec *v1.PodSpec, info []v1.ContainerStatus) v1.PodPhase {
for _, container := range spec.Containers {
containerStatus, ok := podutil.GetContainerStatus(info, container.Name)
if !ok {
unknown++
continue
}
switch {
case containerStatus.State.Running != nil:
running++
case containerStatus.State.Terminated != nil:
stopped++
if containerStatus.State.Terminated.ExitCode == 0 {
succeeded++
}
}
switch {
case running > 0 && unknown == 0:
return v1.PodRunning
case running == 0 && stopped > 0 && unknown == 0:
if spec.RestartPolicy == v1.RestartPolicyAlways {
// All containers are in the process of restarting
return v1.PodRunning
}
if stopped == succeeded {
return v1.PodSucceeded
}
if spec.RestartPolicy == v1.RestartPolicyNever {
return v1.PodFailed
}
return v1.PodRunning
}
}
(8)status_manager中的syncPod方法會(huì)將新的pod狀態(tài)通過patch方法更新到apiserver。
func (m *manager) syncPod(uid types.UID, status versionedPodStatus) {
pod, err := m.kubeClient.CoreV1().Pods(status.podNamespace).Get(context.TODO(), status.podName, metav1.GetOptions{})
newPod, patchBytes, unchanged, err := statusutil.PatchPodStatus(m.kubeClient, pod.Namespace, pod.Name, pod.UID, *oldStatus, mergePodStatus(*oldStatus, status.status))
klog.V(3).InfoS("Patch status for pod", "pod", klog.KObj(pod), "patch", string(patchBytes))
}
mem_alloc代碼
參考極客時(shí)間 《容器高手實(shí)戰(zhàn)課》
[root@localhost mem-alloc]# cat mem_alloc.c
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BLOCK_SIZE (10*1024*1024)
int main(int argc, char **argv)
{
int thr, i;
char *p1;
if (argc != 2) {
printf("Usage: mem_alloc <num (MB)>\n");
exit(0);
}
thr = atoi(argv[1]);
printf("Allocating," "set to %d Mbytes\n", thr);
sleep(10);
for (i = 0; i < thr; i++) {
p1 = malloc(BLOCK_SIZE);
memset(p1, 0x00, BLOCK_SIZE);
}
sleep(600);
return 0;
}
[root@localhost oom]# cat Dockerfile
FROM nginx:latest
COPY ./mem-alloc/mem_alloc /
CMD ["/mem_alloc", "2000"]
[root@localhost oom]# cat Makefile
all: image
mem_alloc: mem-alloc/mem_alloc.c
gcc -o mem-alloc/mem_alloc mem-alloc/mem_alloc.c
image: mem_alloc
docker build -t registry/mem_alloc:v1 .
clean:
rm mem-alloc/mem_alloc -f
docker stop mem_alloc;docker rm mem_alloc;docker rmi registry/mem_alloc:v1
總結(jié)
本文研究的都是確定性的問題园细,好像沒有什么意義惦积,話說回來,如果沒有學(xué)習(xí)這些確定性問題的積累猛频,又如何去應(yīng)對不確定的問題呢狮崩?