zoukankan      html  css  js  c++  java
  • Libev源码分析08:Libev中的信号监视器

             Libev中的信号监视器,用于监控信号的发生,因信号是异步的,所以Libev的处理方式是尽量的将异步信号同步化。异步信号的同步化方法主要有:signalfd、eventfd、pipe、sigwaitinfo等。这里Libev采用的是前三种方法,最终都是将对异步信号的处理,转化成对文件描述符的处理,也就是将ev_signal转化为处理ev_io。

     

    一:数据结构

    1:ev_signal

    typedef struct ev_signal
    {
        int active;
        int pending;
        int priority;
        void *data;
        void (*cb)(EV_P_ struct ev_signal *w, int revents);
        struct ev_watcher_list *next;
    
        int signum;
    } ev_signal;

             ev_signal的结构跟ev_io的结构十分类似,前6个成员是完全一样的,最后一个signum记录信号值。前六个成员构成了一个ev_watcher_list结构,因此信号监视器也是按照链表组织的。

     

    2:ANSIG

    typedef struct
    {
        sig_atomic_t volatile pending;
    #if EV_MULTIPLICITY
        struct ev_loop *loop;
    #endif
        ev_watcher_list *head;
    } ANSIG;
    
    static ANSIG signals [EV_NSIG - 1];

             ANSIG就是Libev内部用来组织ev_signal的结构体,它的成员包括:pending表明该信号是否处于未决状态(触发但尚未处理),head表明该信号对应的监视器链表的头指针,另外,Libev不允许同一个信号出现在多个ev_loop结构中,因此,如果支持多个ev_loop的话,还有一个loop成员记录该信号对应的ev_loop。

     

             signals是ANSIG类型的数组,它的下标就是相应的信号值-1,因此,每个信号都有对应的ANSIG结构。

     

    二:初始化ev_signal

    #define ev_signal_set(ev,signum_)  do {
      (ev)->signum = (signum_); 
    } while (0)
    
    #define ev_signal_init(ev,cb,signum)  do {
       ev_init ((ev), (cb)); 
       ev_signal_set ((ev), (signum)); 
    } while (0)
    

    三:使用signalfd处理信号

             各个系统支持的信号同步机制各有不同,针对多种信号同步机制,Libev采用下面的优先级循序:signalfd、eventfd、pipe。

             signalfd是最简单方便的信号同步机制,可以很容易的将异步的信号的监听转化成对文件描述符的监听。下面首先看一下使用signalfd时的信号处理流程。

     

    1:ev_signal_start

    void ev_signal_start (struct ev_loop *loop, ev_signal *w)
    {
        if (expect_false (ev_is_active (w)))
            return;
            
        assert (("libev: ev_signal_start called with illegal signal number", 
                w->signum > 0 && w->signum < EV_NSIG));
    
    #if EV_MULTIPLICITY
        assert (("libev: a signal must not be attached to two different loops",
                !signals [w->signum - 1].loop || signals [w->signum - 1].loop == loop));
    
        signals [w->signum - 1].loop = loop;
    #endif
    
    #if EV_USE_SIGNALFD
        if (sigfd == -2)
        {
            sigfd = signalfd (-1, &sigfd_set, SFD_NONBLOCK | SFD_CLOEXEC);
            if (sigfd < 0 && errno == EINVAL)
                sigfd = signalfd (-1, &sigfd_set, 0); /* retry without flags */
    
            if (sigfd >= 0)
            {
                fd_intern (sigfd); /* doing it twice will not hurt */
    
                sigemptyset (&sigfd_set);
    
                ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ);
                ev_set_priority (&sigfd_w, EV_MAXPRI);
                ev_io_start (EV_A_ &sigfd_w);
                ev_unref (EV_A); /* signalfd watcher should not keep loop alive */
            }
        }
    
        if (sigfd >= 0)
        {
            sigaddset (&sigfd_set, w->signum);
            sigprocmask (SIG_BLOCK, &sigfd_set, 0);
    
            signalfd (sigfd, &sigfd_set, 0);
        }
    #endif
    
        ev_start (EV_A_ (W)w, 1);
        wlist_add (&signals [w->signum - 1].head, (WL)w);
    
        ...
    
    }

             在ev_signal_start中,首先对信号监视器w进行验证:

    如果它已经被激活,也就是w->active不为0,直接返回;

    信号监视器中的信号值应该处于合法范围(0, EV_NSIG)内,否则进程退出;

    该信号对应的ANSIG结构中记录的ev_loop,应该就是参数loop,否则进程退出;

     

             验证完成后,首先记录一下该信号所在的ev_loop:

    signals [w->signum - 1].loop = loop;

             然后,根据宏EV_USE_SIGNALFD判断系统是否支持signalfd函数,根据loop->sigfd的值判断用户是否使用signalfd函数,在初始化ev_loop的函数loop_init (struct ev_loop *loop, unsigned intflags)中有:

    sigfd = flags & EVFLAG_SIGNALFD ? -2 : -1;

             因此,用户如果想使用signalfd函数,flags参数中必须有EVFLAG_SIGNALFD,也就是在使用ev_default_loop或者ev_loop_new初始化ev_loop时,必须指明EVFLAG_SIGNALFD标志。

     

             如果系统支持signalfd,并且loop->sigfd为-2的话,则开始调用signalfd函数(这是第一次调用signalfd),创建signalfd文件描述符。如果signalfd支持SFD_NONBLOCK和SFD_CLOEXEC标志的话,则直接在signalfd中设置,否则调用fd_intern,使用fcntl设置(即使signalfd支持这俩标志,也会调用该函数重新设置一遍,无伤大雅)。注意,第一次调用signalfd时,信号集(sigset_t)sigfd_set尚未初始化,在下面初始化。

             第一次调用signalfd成功之后,首先清空信号集sigfd_set,然后将signalfd文件描述符加入到Libev内部的IO监视器(ev_io)sigfd_w中,并且启动sigfd_w,注意这里设置sigfd_w的优先级为最高优先级,回调函数为sigfdcb:

    sigemptyset (&sigfd_set);
    
    ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ);
    ev_set_priority (&sigfd_w, EV_MAXPRI);
    ev_io_start (EV_A_ &sigfd_w);
    ev_unref (EV_A); /* signalfd watcher should not keep loop alive */
    

             接下来,将信号w->signum加入到信号集sigfd_set中,阻塞该信号,重新关联signalfd和sigfd_set:

    sigaddset (&sigfd_set, w->signum);
    sigprocmask (SIG_BLOCK, &sigfd_set, 0);
    signalfd (sigfd, &sigfd_set, 0);

             之所以不在调用signalfd的时候阻塞该信号并关联signalfd描述符,是因为所有信号仅使用一个signalfd描述符sigfd,一个IO监视器sigfd_w。当有多个信号监视器时,需要多次调用ev_signal_start,在第一次调用ev_signal_start成功之后,sigfd的值便已是大于等于0的整数了,这样只需要将信号阻塞,然后重新关联sigfd即可,而无需重新创建一个signalfd描述符并加入到IO监视器sigfd_w中。

     

             最后,激活该信号监视器w,并且将其加入到相应的ANSIG结构中:

    ev_start (EV_A_ (W)w, 1);
    wlist_add (&signals [w->signum - 1].head, (WL)w);
    

             这样,如果使用signalfd监控信号,ev_signal_start函数的流程就结束了。接下来,就是监控IO监视器sigfd_w了。当sigfd_set中的一个或多个信号发生时,sigfd变成可读状态,IO监视器sigfd_w触发,在ev_run中,调用ev_invoke_pending时,就会调用它的回调函数sigfdcb。ev_invoke_pending的代码如下:

    void ev_invoke_pending (struct ev_loop *loop)
    {
      pendingpri = NUMPRI;
    
      while (pendingpri) /* pendingpri possibly gets modified in the inner loop */
        {
          --pendingpri;
    
          while (pendingcnt [pendingpri])
            {
              ANPENDING *p = pendings [pendingpri] + --pendingcnt [pendingpri];
    
              p->w->pending = 0;
              EV_CB_INVOKE (p->w, p->events);
              EV_FREQUENT_CHECK;
            }
        }
    }

             该函数中,首先从最高优先级的pendings开始轮训,依次调用其中监视器的回调函数。因IO监视器sigfd_w具有最高优先级,因此如果信号触发了,则sigfd_w的回调函数sigfdcb会首先被调用到。

     

    2:sigfdcb

    static void sigfdcb (struct ev_loop *loop, ev_io *iow, int revents)
    {
        struct signalfd_siginfo si[2], *sip; /* these structs are big */
    
        for (;;)
        {
            ssize_t res = read (sigfd, si, sizeof (si));
    
            /* not ISO-C, as res might be -1, but works with SuS */
            for (sip = si; (char *)sip < (char *)si + res; ++sip)
                ev_feed_signal_event (EV_A_ sip->ssi_signo);
    
            if (res < (ssize_t)sizeof (si))
                break;
        }
    }
    

             该回调函数中,主要是读取sigfd中的信号信息。因struct  signalfd_siginfo结构比较大(128字节),这里采用的技巧是每次read时最多只读取2个。

             针对读取到的信号值,调用ev_feed_signal_event函数。

     

    3:ev_feed_signal_event

    void ev_feed_signal_event (struct ev_loop *loop, int signum)
    {
        WL w;
    
        if (expect_false (signum <= 0 || signum >= EV_NSIG))
            return;
    
        --signum;
    
    #if EV_MULTIPLICITY
      /* it is permissible to try to feed a signal to the wrong loop */
      /* or, likely more useful, feeding a signal nobody is waiting for*/
    
        if (expect_false (signals [signum].loop != EV_A))
            return;
    #endif
    
        signals [signum].pending = 0;
    
        for (w = signals [signum].head; w; w = w->next)
            ev_feed_event (EV_A_ (W)w, EV_SIGNAL);
    }

             在ev_feed_signal_event中,首先检查信号值是否处于合法范围(0, EV_NSIG)内,然后检查该信号对应的ev_loop是否就是当前的loop,如果不是则直接返回。

             然后置signals [signum].pending为0,在signals中找到该信号的监视器列表,针对该列表中的所有监视器,调用ev_feed_event,将监视器加入到loop->pendings中。

             注意,此时添加信号监视器到loop->pendings的流程,还是处于ev_invoke_pending函数的流程中的,因此,在ev_invoke_pending中,处理完sigfd_w监视器后,接着就会处理到刚刚加到loop->pendings的信号监视器。从而信号自己的回调函数就会被调用到。

            

            这样使用signalfd监控信号的完整流程就结束了。

     

    四:使用eventfd、pipe处理信号

             使用eventfd和pipe处理信号的基本思路是一样的,首先创建eventfd描述符或者管道pipe,使用IO监视器监听eventfd描述符或者pipe[0],当信号发生时时,在信号处理程序中,写入eventfd描述符或者pipe[1],从而触发IO监视器,调用回调函数pipecb处理信号。

     

    1:ev_signal_start

             首先看下,当不使用signalfd,或者调用signalfd失败时,ev_signal_start的流程:

    void ev_signal_start (struct ev_loop *loop, ev_signal *w) EV_THROW
    {
        if (expect_false (ev_is_active (w)))
            return;
    
        assert (("libev: ev_signal_start called with illegal signal number", 
                w->signum > 0 && w->signum < EV_NSIG));
    
    #if EV_MULTIPLICITY
        assert (("libev: a signal must not be attached to two different loops",
               !signals [w->signum - 1].loop || signals [w->signum - 1].loop == loop));
    
        signals [w->signum - 1].loop = EV_A;
    #endif
    
    ...
    
        ev_start (EV_A_ (W)w, 1);
        wlist_add (&signals [w->signum - 1].head, (WL)w);
    
        if (!((WL)w)->next)
    # if EV_USE_SIGNALFD
            if (sigfd < 0) /*TODO*/
    # endif
            {
                struct sigaction sa;
    
                evpipe_init (EV_A);
    
                sa.sa_handler = ev_sighandler;
                sigfillset (&sa.sa_mask);
                sa.sa_flags = SA_RESTART; /* if restarting works we save one iteration*/
                sigaction (w->signum, &sa, 0);
    
                if (origflags & EVFLAG_NOSIGMASK)
                {
                    sigemptyset (&sa.sa_mask);
                    sigaddset (&sa.sa_mask, w->signum);
                    sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);
                }
          }
    }
    
            首先对信号监视器w进行验证,然后激活该信号监视器w,并且将其加入到相应的ANSIG结构中,该过程与使用signalfd的流程一样,不再赘述。

            接下来,如果该监视器是当前信号的第一个监视器(((WL)w)->next == NULL),说明这是第一次监听该信号,需要创建该信号的信号处理函数,并且创建eventfd或pipe结构。

            首先调用evpipe_init初始化eventfd或pipe结构,暂且不表,下面详述。

            然后调用sigaction建立该信号的处理函数为ev_sighandler,并且在调用信号处理函数时,阻塞所有信号,且被信号中断的低速系统调用会被重启

    sa.sa_handler = ev_sighandler;
    sigfillset (&sa.sa_mask);
    sa.sa_flags = SA_RESTART; /* if restarting works we save one iteration */
    sigaction (w->signum, &sa, 0);
    

            如果在初始化ev_loop时指定了EVFLAG_NOSIGMASK标志的话,还需要明确将监听的信号解除阻塞。

    if (origflags & EVFLAG_NOSIGMASK)
    {
    sigemptyset (&sa.sa_mask);
    sigaddset (&sa.sa_mask, w->signum);
    sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);
    }
    

     

    2:evpipe_init

             该函数用来创建eventfd描述符或者pipe,并将信号监视器转换为IO监视器pipe_w。代码如下:

    static void evpipe_init (struct ev_loop *loop)
    {
        if (!ev_is_active (&pipe_w))
        {
            int fds [2];
    
    # if EV_USE_EVENTFD
            fds [0] = -1;
            fds [1] = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);
            if (fds [1] < 0 && errno == EINVAL)
                fds [1] = eventfd (0, 0);
    
            if (fds [1] < 0)
    # endif
            {
                while (pipe (fds))
                    ev_syserr ("(libev) error creating signal/async pipe");
    
                fd_intern (fds [0]);
            }
    
            evpipe [0] = fds [0];
    
            if (evpipe [1] < 0)
                evpipe [1] = fds [1]; /* first call, set write fd */
            else
            {
                /* on subsequent calls, do not change evpipe [1] */
                /* so that evpipe_write can always rely on its value. */
                /* this branch does not do anything sensible on windows, */
                /* so must not be executed on windows */
    
                dup2 (fds [1], evpipe [1]);
                close (fds [1]);
            }
    
            fd_intern (evpipe [1]);
    
            ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ);
            ev_io_start (EV_A_ &pipe_w);
            ev_unref (EV_A); /* watcher should not keep loop alive */
        }
    }

            所有信号使用一个IO监视器pipe_w,如果pipe_w已经处于激活状态,则说明相应的结构已经创建好了,直接返回即可。

            然后根据宏EV_USE_EVENTFD判断系统是否支持eventfd,如果支持,则调用eventfd创建eventfd描述符,如果不支持,或者调用eventfd失败,则调用pipe创建管道,并设置管道读端描述符的FD_CLOEXEC和O_NONBLOCK标志。

            使用evpipe[0]记录读描述符,evpipe[1]记录写描述符,如果使用eventfd,则evpipe[0]为-1,evpipe[1]为eventfd描述符,读写描述符都是eventfd。

            调用fd_intern,使用fcntl设置写描述符evpipe[1]的FD_CLOEXEC和O_NONBLOCK标志。

            最后启动内部IO监视器pipe_w,监听读描述符:

    ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ);
    ev_io_start (EV_A_ &pipe_w);
    ev_unref (EV_A); /* watcher should not keep loop alive */
    

            注意,pipe_w监视器的初始化,在loop_init中就已经做了:

    #if EV_SIGNAL_ENABLE || EV_ASYNC_ENABLE
        ev_init (&pipe_w, pipecb);
        ev_set_priority (&pipe_w, EV_MAXPRI);
    #endif
            这里设置pipe_w的回调函数为pipecb,优先级为最高优先级EV_MAXPRI。

     

            当信号产生时,就会调用到信号处理函数ev_sighandler,改处理函数仅仅就是调用函数ev_feed_signal而已。

     

    3:ev_feed_signal

    void ev_feed_signal (int signum)
    {
    #if EV_MULTIPLICITY
        struct ev_loop *loop;
        loop = signals [signum - 1].loop;
    
        if (!loop)
            return;
    #endif
        signals [signum - 1].pending = 1;
        evpipe_write (loop, &sig_pending);
    }
    

             该函数首先根据信号值得到该信号所在的ev_loop,然后置该信号对应的ANSIG的pending为1,最后调用evpipe_write函数。

     

    4:evpipe_write

    void evpipe_write (struct ev_loop *loop, sig_atomic_t volatile *flag)
    {
        if (expect_true (*flag))
            return;
    
        *flag = 1;
    
        pipe_write_skipped = 1;
    
        if (pipe_write_wanted)
        {
            int old_errno;
    
            pipe_write_skipped = 0;
    
            old_errno = errno; /* save errno because write will clobber it*/
    
    #if EV_USE_EVENTFD
            if (evpipe [0] < 0)
            {
                uint64_t counter = 1;
                write (evpipe [1], &counter, sizeof (uint64_t));
            }
            else
    #endif
            {
                write (evpipe [1], &(evpipe [1]), 1);
            }
    
            errno = old_errno;
        }
    }

             注意,当在一个loop中有多个信号发生时,也仅需要产生一个事件而已。本函数主要作用就是当信号发生时向写描述符写入一个事件,即可触发pipe_w中的读事件,表明有一个或者多个信号触发了。但是因为信号处理函数的调用时机是完全随机的,因此,需要有一定的手段保证代码的安全性。

             a:sig_pending

             该值表示是否有信号处于未决状态(触发但尚未处理)。该值在初始化ev_loop时置为0,调用evpipe_write时,会首先判断该值是否为1。如果该值已经为1,表示已经有监听的信号处于未决状态了,无需再向写描述符写入事件了。该值直到调用pipe_w的回调函数pipecb时,消费掉该事件之后才重置为0,表明从此刻起,若有监听信号触发,才能继续向写描述符写入事件。

             b:pipe_write_wanted和pipe_write_skipped

             pipe_write_wanted表明是否允许向写描述符写入事件,pipe_write_skipped表明是否信号发生了,却因pipe_write_wanted的关系被暂时忽略了。这两个值在初始化ev_loop时置为0。

             在evpipe_write中,首先置pipe_write_skipped为1,如果pipe_write_wanted此时为0,evpipe_write直接返回,这就表明触发信号暂时被忽略掉了(仅仅是暂时的)。否则,重置pipe_write_skipped为0,向写描述符写入事件。

             在ev_run中,调用backend_poll之前,会将pipe_write_wanted置为1,表明此刻起写描述符才能接受写入事件,如果此刻之前有监听信号发生的话,则会在信号处理函数调用的evpipe_write中,置pipe_write_skipped为1表示信号暂时忽略掉。

             在ev_run中调用backend_poll后立即置pipe_write_wanted为0。如果pipe_write_skipped为1,表明有信号被忽略了,调用ev_feed_event,直接将pipe_w标记为pending状态,将pipe_w加入到loop->pendings中:

    do{
        ev_tstamp waittime  = 0.;
    
        pipe_write_wanted = 1;
    
        if (expect_true (!(flags & EVRUN_NOWAIT || idleall || !activecnt || pipe_write_skipped)))
        {
            waittime = MAX_BLOCKTIME;
            ...      
        }
    
        backend_poll (EV_A_ waittime);
                
        pipe_write_wanted = 0; /* just an optimisation, no fence needed */
    
        if (pipe_write_skipped)
        {
            assert (("libev: pipe_w not active, but pipe not written", ev_is_active (&pipe_w)));
            ev_feed_event (EV_A_ &pipe_w, EV_CUSTOM);
        }
        
        EV_INVOKE_PENDING;
    }while(expect_true (
        activecnt
        && !loop_done
        && !(flags & (EVRUN_ONCE | EVRUN_NOWAIT))
    ))

             上面就是ev_run中的相关逻辑,注意:调用backend_poll之后,如果在检测pipe_write_skipped之后才有信号发生的话,此时在evpipe_write中仅设置pipe_write_skipped为1后就返回。然后进入下次循环,因pipe_write_skipped为1,所以waittime为0,backend_poll会立即返回,处理pipe_w的激活事件。

             PS:现在还没有想明白,为什么需要pipe_write_wanted和pipe_write_skipped这两个标志,感觉sig_pending已经足够了。


             在向写描述符写入事件之后,pipe_w监视器触发,调用回调函数pipecb。

     

    5:pipecb

    static void pipecb (struct ev_loop *loop, ev_io *iow, int revents)
    {
        int i;
    
        if (revents & EV_READ)
        {
    #if EV_USE_EVENTFD
            if (evpipe [0] < 0)
            {
                uint64_t counter;
                read (evpipe [1], &counter, sizeof (uint64_t));
            }
            else
    #endif
            {
                char dummy[4];
                read (evpipe [0], &dummy, sizeof (dummy));
            }
        }
    
        pipe_write_skipped = 0;
    
    #if EV_SIGNAL_ENABLE
        if (sig_pending)
        {
            sig_pending = 0;
    
            for (i = EV_NSIG - 1; i--; )
                if (expect_false (signals [i].pending))
                    ev_feed_signal_event (EV_A_ i + 1);
        }
    #endif
    ...
    }

             在pipecb中,首先从eventfd描述符或者pipe[0]中消费掉事件。然后轮训signals数组中每个ANSIG结构的pending字段,只要是信号触发了,则该字段一定为1,从而可以调用ev_feed_signal_event处理该信号。剩下的流程就与使用signalfd时一样了,不再赘述。

     

    四:ev_signal_stop

    void ev_signal_stop (struct ev_loop *loop, ev_signal *w)
    {
        clear_pending (EV_A_ (W)w);
        if (expect_false (!ev_is_active (w)))
            return;
    
        wlist_del (&signals [w->signum - 1].head, (WL)w);
        ev_stop (EV_A_ (W)w);
    
        if (!signals [w->signum - 1].head)
        {
    #if EV_MULTIPLICITY
            signals [w->signum - 1].loop = 0; /* unattach from signal */
    #endif
    #if EV_USE_SIGNALFD
            if (sigfd >= 0)
            {
                sigset_t ss;
    
                sigemptyset (&ss);
                sigaddset (&ss, w->signum);
                sigdelset (&sigfd_set, w->signum);
    
                signalfd (sigfd, &sigfd_set, 0);
                sigprocmask (SIG_UNBLOCK, &ss, 0);
            }
            else
    #endif
                signal (w->signum, SIG_DFL);
        }
    }

             在ev_signal_stop中,首先调用clear_pending清除监视器w在loop->pendings中的状态,置w->pending = 0。这里有个技巧是将loop->pendings中,原w所在位置直接赋值为内部伪监视器pending_w,pending_w的回调函数为空函数,会直接返回。

             调用wlist_del,将w从该信号的监视器列表中删除,调用ev_stop注销改监视器;

             如果相应信号的监视器列表空了,则首先signals [w->signum - 1].loop =0,然后恢复该信号的处理方式:若使用signalfd,则取消阻塞该信号,将该信号从sigfd描述符关联的信号集中删除;若不使用signalfd,则直接恢复该信号的处理方式为默认方式。

     

    五:总结



    六:例子

    ev_signal signal_w;
    
    void signal_action(struct ev_loop *main_loop,ev_signal *signal_w,int e)
    {
        puts("
    in signal cb 
    ");
    }
    
    int main()
    {
        struct ev_loop *main_loop = ev_default_loop(EVFLAG_SIGNALFD);
          
        ev_init(&signal_w,signal_action);
        ev_signal_set(&signal_w,SIGINT); 
    
        ev_signal_start(main_loop,&signal_w);
    
        ev_run(main_loop,0);
        return 0;
    }
    


             结果:

    #./a.out
    ^C
    in signal cb 
    
    ^C
    in signal cb 
    
    ^Quit (core dumped)
    

  • 相关阅读:
    Django 之DRF删改查的API接口
    Django 之DRF原理操作
    通过密码、username、时间戳进行md5加密简单脚本
    Java 之GC常用命令
    ORM 之常用查询、字段类型、约束
    ORM 之单表查询案例
    Django session使用
    K8S 之yaml文件参数介绍1
    K8S 之使用Capabilities与限制网络带宽
    K8S 之容器生命周期钩子
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247095.html
Copyright © 2011-2022 走看看