前沿

大家都知道对linux系统来说1号进程为init进程,是由0号进程(内核进程)通过调用系统init函数创建的第一个用户进程1进程,主要做用户态进程的管理,垃圾回收等动作。
对docker来讲1号进程大多数情况下都是服务进程,或者是用户自己开发的服务daemon进程,这也是瘦容器的理论,那服务进程作为1号进程有什么区别呢?
本文详细讲述1号进程的区别,如何规避僵尸进程等

docker的进程管理

docker进程管理的基础是LINUX内核中的PID命名空间技术,在不同PID名空间中,进程ID是独立的;即在两个不同名空间下的进程可以有相同的PID。
在Docker中,每个Container都是Docker Daemon的子进程,每个Container进程缺省都具有不同的PID名空间。通过名空间技术,Docker实现容器间的进程隔离。
当创建一个Docker容器的时候,就会新建一个PID名空间。容器启动进程在该名空间内PID为1。当PID1进程结束之后,Docker会销毁对应的PID名空间,并向容器内所有其它的子进程发送SIGKILL。

下面通过例子具体看docker进程情况:
容器内执行ps -ef,可以看到1号进程为服务daemon进程redisadmin及其子进程redis-server

[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379

在宿主机上通过docker top container_id可以看到容器内进程,从下面可以卡看到1号进程redisadmin在宿主机上父进程为docker daemon

[root@localhost ~]# docker top 61570e71c903
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
2000                436                 32147               0                   07:48               ?                   00:00:00            redis-server *:6379
2000                32147               32130               0                   06:50               pts/4               00:00:01            python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
  • 知识1:通过exec执行的docker命令,父进程为0号进程,也就是docker daemon进程,容器内的0号进程,不是1号进程。 但是由exec启动的进程属于容器的namespace和相应的cgroup
   [root@localhost ~]# docker exec -ti 61570e71c903 bash   用bash进入容器[hitv@localhost data]$ ps -efUID        PID  PPID  C STIME TTY          TIME CMDhitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmihitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379hitv      5056     0  0 07:57 ?        00:00:00 bash   发现bash的父进程为0号[hitv@localhost data]$ exit[root@localhost ~]# docker exec 61570e71c903 sleep 1000&  用exec到docker里后台执行sleep[root@localhost ~]# docker exec 61570e71c903 ps -efUID        PID  PPID  C STIME TTY          TIME CMDhitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmin -d startup -P 6379 -fhitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379hitv      5125     0  0 07:58 ?        00:00:00 sleep 1000    发现sleep进程的父进程也是0号

上面的这个结论在下面将僵尸进程时会用到,请留意

  • 知识点2:由于PID1进程的特殊性,Linux内核为他做了特殊处理。如果它没有提供某个信号的处理逻辑,那么与其在同一个PID名空间下的进程发送给它的该信号都会被屏蔽。这个功能的主要作用是防止init进程被误杀
    [root@localhost ~]# docker exec 61570e71c903 kill -9 1[root@localhost ~]# docker exec 61570e71c903 ps -efUID        PID  PPID  C STIME TTY          TIME CMDhitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmin -d startup -P 6379 -fhitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379hitv      5125     0  0 07:58 ?        00:00:00 sleep 1000hitv      5545     0  0 08:04 ?        00:00:00 ps -ef
上面可以看到执行kill -9是没用的,是杀不掉1号进程的,验证了上面的知识点2
如果要想能在容器内执行kill干掉1号进程,需要在1号进程中实现信号接收处理,比如收到kill -15执行exit操作,python例子如下
    def sigterm_handler(sig, frame):logging.info("get term signal({0})".format(sig))if sig == 15:logging.info("get kill -15, exit")sys.exit(1)signal.signal(signal.SIGTERM, sigterm_handler)
  • 附加知识点: 自从Docker 1.5之后,docker run命令引入了–pid=host参数来支持使用宿主机PID名空间来启动容器进程,这样可以方便的实现容器内应用和宿主机应用之间的交互:比如利用容器中的工具监控和调试宿主机进程。

指定docker的1号进程

可以被Dockerfile中的ENTRYPOINT或CMD指令所指明;也可以被docker run命令的启动参数所覆盖
这里主要描述在ENTRYPOINT和CMD指令中,提供两种不同的进程执行方式 shell 和 exec

  • shell 方式 CMD executable param1 param2
    CMD redisadmin -d startup -P 6379 -f
注意:如果redisadmin是shell脚本,则启动方式为/bin/sh -c ”redisadmin -d startup -P 6379 -f”,这样1号进程就为/bin/sh,1号进程中拉起的redisadmin -d startup -P 6379 -f
但是我的redisadmin为python的,首行写的#!/usr/bin/env python3,所以从上面例子可以看到1号进程为python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
这样实际上跟exec方式一样了。
  • exec 方式 CMD [“executable”,“param1”,“param2”] 这种方式跟run命令的启动参数覆盖的1号进程一样,写的什么什么就是1号进程
  • 这两种方式的具体不同在哪里?
    • 这里就用到了进程管理那部分的知识,PID1进程对于操作系统而言具有特殊意义。操作系统的PID1进程是init进程,以守护进程方式运行,是所有其他进程的祖先,具有完整的进程生命周期管理能力。在Docker容器中,PID1进程是启动进程,它也会负责容器内部进程管理的工作。而这也将导致进程管理在Docker容器内部和完整操作系统上的不同。
    • 不同1: 到底谁负责进程管理,比如我redisadmin为1号,我在里面写了很多子进程处理的代码,但是shell方式他的1号进程是bash,就会导致无法做处理了
    • 不同2:Docker提供了两个命令docker stop和docker kill来向容器中的PID1进程发送信号
      • 当执行docker stop命令时,docker会首先向容器的PID1进程发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有结束, 那么Docker Daemon会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死变为退出状态。

        • 也就是说如果我在1号进程实现了SIGTERM(15)信号处理,比如不是上面那样简单的退出自己,而是先优雅的对redis执行shutdown(redis做bgsave保证数据不丢失),然后再sys.exit(1),这样就实现了容器优雅stop
      • 而docker kill可以向容器内PID1进程发送任何信号,缺省是发送SIGKILL信号来强制退出应用,当然这里并不是用exec执行的,上面提到过exec是干不掉1号进程的,这里在宿主机上对1号进程下发的kill

僵尸进程

当一个子进程终止后,它首先会变成一个“失效(defunct)”的进程,也称为“僵尸(zombie)”进程,等待父进程或系统收回(reap)。在Linux内核中维护了关于“僵尸”进程的一组信息(PID,终止状态,资源使用信息),从而允许父进程能够获取有关子进程的信息。如果不能正确回收“僵尸”进程,那么他们的进程描述符仍然保存在系统中,系统资源会缓慢泄露。
僵尸进程:终止的进程但是因为父进程没有垃圾回收功能导致的进程,跟孤儿进程的区别是孤儿进程知识父进程退出了,但是自己还未终止
孤儿进程:子进程未退出,但是父进程退出了,这种进程会变为孤儿进程,在Linux中Init进程(PID1)作为所有进程的父进程,会维护进程树的状态,一旦有某个子进程成为了“孤儿”进程后,init就会负责接管这个子进程。当一个子进程成为“僵尸”进程之后,如果其父进程已经结束,init会收割这些“僵尸”,释放PID资源。
下面主要讲解docker中1号进程为服务进程不是init进程时是什么表现,该怎么处理僵尸进程

情况1: exec启动的进程

[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
[root@localhost ~]# docker exec 61570e71c903 sleep 1000 &
[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8557     0  3 08:45 ?        00:00:00 sleep 1000
[root@localhost ~]# docker exec 61570e71c903 kill -9 8557
[1]+  Exit 137                docker exec 61570e71c903 sleep 1000
[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379

由进程管理那部分我们得出的结论是exec执行的进程其父进程是0号进程也就是docker daemon,docker daemon进程有垃圾回收,所以不会产生僵尸进程

情况2: bash启动的进程,bash进程不退出

窗口1

[root@localhost ~]# docker exec -ti 61570e71c903 bash   进入容器
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  2 08:48 ?        00:00:00 bash   这个就为进入容器的bash进程
[hitv@localhost data]$ sleep 1000&
[1] 8824
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
hitv      8824  8785  0 08:48 ?        00:00:00 sleep 1000  启动sleep进程 发现父进程为bash进程

窗口2

[root@localhost ~]# docker exec -ti 61570e71c903 bash  进入容器
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash    窗口1的bash进程
hitv      8824  8785  0 08:48 ?        00:00:00 sleep 1000  窗口1bash拉起的sleep进程
hitv      8874     0  1 08:49 ?        00:00:00 bash  自己窗口的bash进程
[hitv@localhost data]$ kill -9 8824  kill掉sleep进程
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      8907  8874  0 08:49 ?        00:00:00 ps -ef

从窗口2kill掉sleep进程,发现sleep进程被彻底清理了,这是因为sleep的父进程窗口1的bash进程是有子进程垃圾回收机制的

情况3:bash启动的进程,bash进程退出

窗口1

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash  窗口1bash
hitv      8874     0  0 08:49 ?        00:00:00 bash  窗口2bash
[hitv@localhost data]$ sleep 1000&   窗口1启动sleep进程
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      9342  8785  0 08:55 ?        00:00:00 sleep 1000 父进程为窗口1bash

窗口2

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash   窗口1bash
hitv      8874     0  0 08:49 ?        00:00:00 bash   窗口2bash
hitv      9342  8785  0 08:55 ?        00:00:00 sleep 1000   窗口1bash启动的sleep
[hitv@localhost data]$ kill -9 8785   kill掉窗口1bash 让sleep1000成为孤儿进程
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      9342     0  0 08:55 ?        00:00:00 sleep 1000  非常震惊的发现孤儿进程被0号进程接管而不是1号进程
hitv      9435  8874  0 08:56 ?        00:00:00 ps -ef
[hitv@localhost data]$ kill -9 9342   kill掉孤儿进程
[hitv@localhost data]$ ps -ef    发现没有成为僵尸,因为0号进程docker daemon把终止的sleep进程回收了
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      9526  8874  0 08:57 ?        00:00:00 ps -ef

这里的疑问是为什么孤儿进程sleep没被1号进程接管,而是被0号进程接管了呢?
按照宿主机的理论这里应该要被1号进程接管才对,但是docker表现的是被0号接管了,通过查询资料发现:
Docker1.11版本之前孤儿进程是由容器内pid为1的进程接收,而1.11版本后是由docker-containerd-shim进程接收,docker-containerd-shim进程时有进程管理功能的,所以这时候kill掉sleep也不会出现僵尸进程
参考自:https://blog.csdn.net/liukuan73/article/details/78043928

由上面理论可见docker中产生僵尸进程的唯一一个点就是1号进程拉起的进程,在1号进程没退出且没有子进程管理时的场景
所以1号进程如果开辟多进程一定要有进程管理功能,下面继续做此验证

情况4:1号进程拉起的子进程,子进程退出的场景

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
[hitv@localhost data]$ kill -9 4398   kill掉redis进程,等待父进程回收
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 [redis-server *:6379] <defunct>
hitv      8874     0  0 08:49 ?        00:00:00 bash

在1号进程没有没有进程管理时,发现redis进程果然成为了僵尸进程,这是docker出现僵尸进程最常见的场景,所以1号进程一定要有进程管理,下面介绍如何添加子进程清理

1号进程添加僵尸进程清理

python为例

def wait_child(sig, frame):try:while True:#收到信号就检查一下有没有子进程要处理, 其中os.WNOHANG表示不阻塞,就类似于wait()只是不阻塞child_pid, status = os.waitpid(-1, os.WNOHANG) if child_pid == 0:logging.debug('No child process need to wait')breakexitcode = status >> 8logging.debug('child process {0} exit, exitcode {1}'.format(child_pid, exitcode))except OSError as e:# 当没有要处理的子进程时会进入这里if e.errno == errno.ECHILD: logging.debug("No child processes")else:logging.info("wait_child error: {0}".format(e))signal.signal(signal.SIGCHLD, wait_child)  子进程退出时都会向主进程发送SIGCHLD信号,这里捕获这个信号给wait_child()处理

测试

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:04 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:07 redis-server *:6379
hitv      8874     0  0 08:49 ?        00:00:00 bash
[hitv@localhost data]$ kill -9 4398
[hitv@localhost data]$ ps -ef   不再僵尸了
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:04 python3 /usr/local/bin/redisadmi
hitv      8874     0  0 08:49 ?        00:00:00 bash

总结

  • docker pid1与宿主机pid1有所不同
  • Docker1.11版本之前孤儿进程是由容器内pid为1的进程接收,而1.11版本后是由docker-containerd-shim进程接收,可以减少因1号进程没有子进程处理导致的僵尸进程
  • docker pid1进程的启动也有两种不同,鼓励使用exec避免造成了不符合预期的现象
  • docker exec产生的进程父进程是0号
  • docker pid1进程得实现下信号处理,不然无法在同PID名空间内向其发送信号退出
  • docker pid1进程得实现下子进程清理,避免出现僵尸进程

参考

https://www.cnblogs.com/ilinuxer/p/6188303.html
https://blog.csdn.net/liukuan73/article/details/78043928

docker进程管理(1号进程,僵尸进程详解)相关推荐

  1. learn.log - 进程管理器fastcgi原理及fastcgi_param详解

    一. 何为FastCGI?  in all : 快-不崩溃-优雅 fast-strong-high FastCGI官方站点:http://www.fastcgi.com.common gateway  ...

  2. (王道408考研操作系统)第二章进程管理-第二节4:调度算法详解2(RR、HPF和MFQ)

    文章目录 一:时间片轮转调度算法(RR) 二:优先级调度算法(HPF) 三:多级反馈队列调度算法(MFQ) 总结 进程调度算法也称为CPU调度算法,操作系统内存在着多种调度算法,有的调度算法适用于作业 ...

  3. (王道408考研操作系统)第二章进程管理-第二节3:调度算法详解1(FCFS、SJF和HRRN)

    文章目录 一:先来先服务调度算法(FCFS) 二:最短作业优先调度算法(SJF)和最短剩余时间优先算法(SRTN) (1)最短作业优先调度算法(SJF) (2)最短剩余时间优先算法(SRTN) 三:高 ...

  4. (王道408考研操作系统)第二章进程管理-第一节3:进程控制(配合Linux讲解)

    文章目录 一:如何实现进程控制 二:进程控制原语 (1)进程创建 A:概述 B:补充-Linux中的创建进程操作 ①:fork() ②:fork()相关问题 (2)进程终止 A:概述 B:补充-僵尸进 ...

  5. python僵尸进程和孤儿进程_进程3.0——进程状态与僵尸进程、孤儿进程

    进程3.0--进程状态与僵尸进程.孤儿进程 进程状态 一个进程的生命周期可以划分为一组状态,这些状态刻画了整个进程.进程状态即体现一个进程的生命状态 一般来说,进程有五种状态:创建状态:进程在创建时需 ...

  6. Linux | 进程概念、进程状态(僵尸进程、孤儿进程、守护进程)、进程地址空间

    文章目录 进程和程序 操作系统如何控制和调度程序 进程控制块–PCB 子进程 进程状态 僵尸进程 孤儿进程 守护进程(精灵进程) 进程地址空间 引言 页表 进程和程序 程序: 一系列有序的指令集合(就 ...

  7. linux mysql 僵尸进程_linux shell中清理僵尸进程

    今天登录到服务器上时,系统打印有6 zombie processes存在,于是用kill -9去清理掉这些僵尸进程,命令执行完后没有错误,可是再次查找时,发现僵尸进程仍然存在,不知道怎么清理了,上网找 ...

  8. linux ps -ef哪一位是进程号,Linux ps 命令详解

    (此文章为收集网络IT达人们博文中有用信息后,整理出来的,感谢他们)(PS:追加感谢 by lxrm) ps  aux详细解释ps aux 显示其他用户启动的进程(a) 查看系统中属于自己的进程(x) ...

  9. php apache 多进程,php多进程 防止出现僵尸进程 如何 使 apache 成为 僵尸进程

    php pcntl 僵尸进程怎么产生的一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退 ...

  10. docker实践(2)常用命令和DockerFile详解

    <docker实践(1) 入门和springBoot实践部署> <docker实践(2)常用命令和DockerFile详解> <docker实践(3) 仓库registr ...

最新文章

  1. 计算机应用基础第三章操作步骤,最新江西三校生计算机应用基础模拟操作题集锦(超实用!)...
  2. 根据标签分布来选择损失函数
  3. [k8s]k8s pod的4种网络模式最佳实战(externalIPs )
  4. Ansible Playbook详解
  5. access表怎么生成表结构_数据结构——单链表讲解
  6. log4j2+ELK
  7. c语言中浮点数如何声明,C语言中浮点数定义和文本处理的配合
  8. 第一周周冠军带你解析赛题,尝试广告算法新思路
  9. nginx搭建文件服务器
  10. java setmethod_java.util.zip.ZipEntry.setMethod(int method)方法示例
  11. 公众号推送长图最佳尺寸_微信公众平台图片尺寸是多少
  12. oracle 11g 重置,oracle数据库重置
  13. lisp绘制法兰_lisp语言画键槽_用LISP语言自定义AutoCAD命令
  14. 迷宫问题 深度优先搜索【c++】
  15. 黑马程序员————集合2(day17)
  16. ctf中linux 内核态的漏洞挖掘与利用系列
  17. steamvr自定义按键_Steam入门手册:教你如何自定义Steam VR中的手柄皮肤
  18. conda创建指定python版本environment
  19. [附源码]JAVA毕业设计人才库构建研究(系统+LW)
  20. iOS 下载功能(断点续传)

热门文章

  1. python改变图片透明度_Python PIL.Image之修改图片背景为透明
  2. photoshop制作透明背景图片1
  3. matlab支持向量机预测电机故障,关于支持向量机(SVM)的一个简单应用实例及matlab代码...
  4. 计算机博士自白:毕业放弃学术去企业,从天之骄子坠落成天生白痴
  5. 【项目复习篇】EGO电商项目技术总结与学习笔记
  6. 网络广告CPS/CPC/CPV/CPM/CPA分别是什么意思
  7. 设计模式之装饰器模式
  8. 关于Matlab中括号用法的总结
  9. 网站制作必备-在线按钮生成器,LOGO生成器,背景生成器,ICO图标生成器,和许多在线小工具...
  10. JS ShadowDOM组件修改样式,添加事件