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("\nin signal cb \n");
} 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)
Libev源码分析08:Libev中的信号监视器的更多相关文章
- Libev源码分析08:Libev中的内存扩容方法
在Libev中,如果某种结构的数组需要扩容,它使用array_needsize宏进行处理,比如: array_needsize (int, fdchanges, fdchangemax, fdchan ...
- [转]Libev源码分析 -- 整体设计
Libev源码分析 -- 整体设计 libev是Marc Lehmann用C写的高性能事件循环库.通过libev,可以灵活地把各种事件组织管理起来,如:时钟.io.信号等.libev在业界内也是广受好 ...
- angular源码分析:angular中脏活累活的承担者之$interpolate
一.首先抛出两个问题 问题一:在angular中我们绑定数据最基本的方式是用两个大括号将$scope的变量包裹起来,那么如果想将大括号换成其他什么符号,比如换成[{与}],可不可以呢,如果可以在哪里配 ...
- angular源码分析:angular中入境检察官$sce
一.ng-bing-html指令问题 需求:我需要将一个变量$scope.x = '<a href="http://www.cnblogs.com/web2-developer/&qu ...
- angular源码分析:angular中各种常用函数,比较省代码的各种小技巧
angular的工具函数 在angular的API文档中,在最前面就是讲的就是angular的工具函数,下面列出来 angular.bind //用户将函数和对象绑定在一起,返回一个新的函数 angu ...
- angular源码分析:angular中的依赖注入式如何实现的
一.准备 angular的源码一份,我这里使用的是v1.4.7.源码的获取,请参考我另一篇博文:angular源码分析:angular源代码的获取与编译环境安装 二.什么是依赖注入 据我所知,依赖注入 ...
- Libev源码分析09:select突破处理描述符个数的限制
众所周知,Linux下的多路复用函数select采用描述符集表示处理的描述符.描述符集的大小就是它所能处理的最大描述符限制.通常情况下该值为1024,等同于每个进程所能打开的描述符个数. 增大描述符集 ...
- angular源码分析:angular中$rootscope的实现——scope的一生
在angular中,$scope是一个关键的服务,可以被注入到controller中,注入其他服务却只能是$rootscope.scope是一个概念,是一个类,而$rootscope和被注入到cont ...
- angular源码分析:angular中jqLite的实现——你可以丢掉jQuery了
一.从function JQLite(element)函数开始. function JQLite(element) { if (element instanceof JQLite) { //情况1 r ...
随机推荐
- Java中字符串为什么不以\0结尾
Java中字符串为什么不以\0结尾 其实这个问题没有什么好说的,Java里面一切都是对象,是对象的话,字符串肯定就有长度,即然有长度,编译器就可以确定要输出的字符个数,当然也就没有必要去浪费那1字节的 ...
- 提升mysql服务器性能(HA MMM MHA MaxScale)
原文:提升mysql服务器性能(HA MMM MHA MaxScale) 版权声明:皆为本人原创,复制必究 https://blog.csdn.net/m493096871/article/detai ...
- 为GitLab配置邮件服务
修改配置文件:/etc/gitlab/gitlab.rb ####################################################################### ...
- JSP-JSP
JSP(Java Server Page) 1 JSP简介 2 JSP脚本和注释 3 JSP的运行原理 jsp本质上就是Servlet 看在服务器里面提应用就应该明白了 我们可以看下这个源码 目录地址 ...
- PhpSpreadsheet处理表格2
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8& ...
- jsp页面_按回车键触发事件
一般在列表页面中,都会带有查询按钮,当输入完查询条件后,如果需要通过鼠标点击"查询"按钮才发起查询,那么就感觉不够方便,那么我们就可以修改为按下回车键的时候发起查询. <sc ...
- 通过sql 向数据库插入多行语句
我们知道通过insert into 表名(列名) values(值)是向表中插入一条语句,可是当我们需要向数据库插入多条语句时,应该怎么做呢? 可以通过如下格式的sql 语句来实现一次向数据库插入多行 ...
- selenium(3):学习操作元素基本方法
来源:<selenium webdriver基于Python源码案例> 一.打开网页①第一步:从selenium里面导入webdriver模块②打开浏览器(Ie和Chrome对应下面的)③ ...
- Codeforces 356A
这题有个注意的地方,就是对集合边读边删除的时候,应该尤为注意.. my_set.erase(it++) #include <iostream> #include <cstring ...
- const、引用与指针
前提 我们忽略掉了相同类型是否可以赋值的情况(我到现在的学习里都还可以相互赋值),以及类型兼容的情况.只考虑const.&.*等修饰符带来的影响 类型兼容: 强制类型转换 基类与子类间的兼容 ...