08 系统调用

进程间通信

应用场景

  • 数据传输 :一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
  • 共享数据 :多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
  • 通知事件 :一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享 :多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
  • 进程控制 :有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

实现方式

  • 管道 (pipe),管道包括三种:
    • 普通管道 PIPE: 通常有两种限制, 一是单工, 只能单向传输; 二是只能在父子或者兄弟进程间使用.
    • 流管道 s_pipe: 去除了第一种限制, 为半双工,只能在父子或兄弟进程间使用,可以双向传输.
    • 命名管道 name_pipe:去除了第二种限制, 可以在许多并不相关的进程之间进行通讯.
  • 信号量 (semophore)是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列 (message queue)是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 (signal)是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存 (shared memory)就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  • 套接字 (socket)是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

标准库

syscall包,一个用于底层操作系统原语的接口,其中定义了所有的系统调用看这里os/signal包,封装信号实现对输入信号的访问。

标准定义

在POSIX.1-1990标准中定义的信号列表

信号动作说明
SIGHUP1Term终端控制进程结束(终端连接断开)
SIGINT2Term用户发送INTR字符(Ctrl+C)触发
SIGQUIT3Core用户发送QUIT字符(Ctrl+/)触发
SIGILL4Core非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT6Core调用abort函数触发
SIGFPE8Core算术运行错误(浮点运算错误、除数为零等)
SIGKILL9Term无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV11Core无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE13Term消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM14Term时钟定时信号
SIGTERM15Term结束程序(可以被捕获、阻塞或忽略)
SIGUSR130,10,16Term用户保留
SIGUSR231,12,17Term用户保留
SIGCHLD20,17,18Ign子进程结束(由父进程接收)
SIGCONT19,18,25Cont继续执行已经停止的进程(不能被阻塞)
SIGSTOP17,19,23Stop停止进程(不能被捕获、阻塞或忽略)
SIGTSTP18,20,24Stop停止进程(可以被捕获、阻塞或忽略)
SIGTTIN21,21,26Stop后台程序从终端中读取数据时触发
SIGTTOU22,22,27Stop后台程序向终端中写数据时触发

在SUSv2和POSIX.1-2001标准中的信号列表:

信号动作说明
SIGTRAP5CoreTrap指令触发(如断点,在调试器中使用)
SIGBUS0,7,10Core非法地址(内存地址对齐错误)
SIGPOLLTermPollable event (Sys V). Synonym for SIGIO
SIGPROF27,27,29Term性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS12,31,12Core无效的系统调用(SVr4)
SIGURG16,23,21Ign有紧急数据到达Socket(4.2BSD)
SIGVTALRM26,26,28Term虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU24,24,30Core超过CPU时间资源限制(4.2BSD)
SIGXFSZ25,25,31Core超过文件大小资源限制(4.2BSD)

Linux kill命令

sugoi@sugoi:~$ kill --help
kill: kill [-s 信号声明 | -n 信号编号 | -信号声明] 进程号 | 任务声明 ... 或 kill -l [信号声明]
    向一个任务发送一个信号。

    向以 PID 进程号或者 JOBSPEC 任务声明指定的进程发送一个以
    SIGSPEC 信号声明或 SIGNUM 信号编号命名的信号。如果没有指定
    SIGSPEC 或 SIGNUM,那么假定发送 SIGTERM 信号。

    选项:
      -s sig    SIG 是信号名称
      -n sig    SIG 是信号编号
      -l        列出信号名称;如果参数后跟 `-l'则被假设为信号编号,
                而相应的信号名称会被列出

    Kill 成为 shell 内建有两个理由:它允许使用任务编号而不是进程号,
    并且在可以创建的进程数上限达到是允许进程被杀死。

    退出状态:
    返回成功,除非使用了无效的选项或者有错误发生。

sugoi@sugoi:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

killkill -9

获取PID的方式,执行这些命令ps/ps/pidof/pstree/top

  • kill pid的作用是向进程号为pid的进程发送SIGTERM这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。

  • kill -9 pid则是向进程号为pid的进程发送 SIGKILL(该信号的编号为9),SIGKILL信号既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。所以,应用程序根本无法“感知”SIGKILL信号,在完全无准备的情况下,被收到SIGKILL信号的操作系统给终止,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。

事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。

在某些情况下(如进程已经hang死,无法响应正常信号),就可以使用 kill -9 来结束进程。

系统调用的应用

应用程序优雅退出

Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态,在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,也就是希望进程优雅退出(exit gracefully)。

Go中对信号的处理主要使用os/signal包中的两个方法:

  • notify方法用来监听收到的信号,func Notify(c chan<- os.Signal, sig ...os.Signal)
  • stop方法用来取消监听,func Stop(c chan<- os.Signal)

os包中对syscall包中的几个常用信号进行了封装,如下:

var (
    Interrupt Signal = syscall.SIGINT
    Kill      Signal = syscall.SIGKILL
)
package main

import (
    "fmt"
    "os"
    "os/signal"
)

func main() {

s := &http.Server{
 Addr:           ":8080",
 Handler:        myHandler,
 ReadTimeout:    10 * time.Second,
 WriteTimeout:   10 * time.Second,
 MaxHeaderBytes: 1 << 20,
}

log.Fatal(s.ListenAndServe())

 c := make(chan os.Signal) // 设置发送信号通知的通道。如果在发送信号时还没有准备好接收信号,则必 使用缓冲通道,否则可能会丢失信号
 signal.Notify(c)          // 不将任何信号传递给通知意味着将所有信号发送到通道
 signal.Notify(c, os.Interrupt)  // 监听Interrupt即syscall.SIGINT信号,用户发送INTR字 (Ctrl+C)触发
 fmt.Println("启动了程序")
 s := <-c // 阻塞直到收到信号
 fmt.Println("收到信号:", s)

 ctx := context.Background()
 log.Println("shut:",s.Shutdown(ctx))
}

优雅的退出一般就是根据需要监听多个信号。

应用程序热更新

在服务器不停机状态下,对正常访问流程不造成干扰和影响的程序升级方式。主要指的是服务程序的热更新,并不是 APP 和操作系统的 HotFix 技术。

互联网服务都追求服务高可用,即能提供 7x24 不间断服务,升级代码影响用户是不可以接受的。一般都追求 SLA 达标 99.99%,即一年故障时间不能超过 1 小时,而几乎每天都会进行代码升级,因此必须采用热更新。后端服务往往都是用集群承担大规模流量,代码升级往往涉及很多台机器,人工升级肯定是不现实的,需要自动化的升级方式。

热更新的目标

  1. 不关闭现有连接(正在运行中的程序)
  2. 新的进程启动并替代旧进程
  3. 新的进程接管新的连接
  4. 连接要随时响应用户的请求
    1. 当用户仍在请求旧进程时要保持连接
    2. 新用户应请求新进程,不可以出现拒绝请求的情况

Nginx热更新原理

Nginx进程及控制信息:

  • Master:
    • 监控worker进程:CHLD
    • 管理worker进程:TREM,INT,QUIT,HUP,USER1,USER2,WINCH
  • worker:TREM,INT,QUIT,HUP,USER1,WINCH
  • nginx命令行:
    • reload:HUP
    • reopen:USER1
    • stop:TERM
    • quit:QUIT

热更新流程:

  1. 向master进程发生HUP信息(reload命令)
  2. master进程校验配置语法是否正确
  3. master进程打开新的监听端口
  4. master进程用新配置启动新的woker子进程
  5. master进程向老worker子进程发生QUIT信号
  6. 老worker进程关闭监听句柄,处理完当前连接后结束进程

golang热更新

  1. 主动流量调度:API网关+CI/CD:发布时自动摘除机器,等待程序处理完现有请求再做发布处理,或者使用 SLB 或者 LVS 手动切换流量。
  2. 程序优雅重启:保证在重启的时候 listen socket FD(文件描述符) 依然可以接受请求,只不过切换新老进程
  3. kubernetes滚动升级
  4. 标准库函数优雅关闭 func (srv *Server) Shutdown(ctx context.Context) error
上次修改: 14 May 2020