场景

我们在编写部署系统的时候,通常需要在机器上部署一个agent,用来执行部署脚本,为了防止部署脚本写的有问题,长时间hang住,我们通常会为脚本的执行设置一个超时时间,到了时间之后就kill掉该脚本的进程。如果是Go语言实现,脑袋里应该立马浮现出os/exec包、cmd.Process.Kill()这样的手段。但是,如果部署脚本中又调用了其他脚本,即子进程又fork出更多子进程的时候,这招就不好使了。

简单来说,就是cmd.Process.Kill()无法杀死子进程。

知识储备

pstree -g		#查看进程树和每个进程的PGID

问题验证

下面我们写段代码来简单验证一下

一般情况下:

package main

import (
"fmt"
"os/exec"
"time"
) func main() {
cmd := exec.Command("sleep", "5") //睡眠5s
start := time.Now() //记录启动时间
time.AfterFunc(3*time.Second, func() { cmd.Process.Kill() }) //3s后将此进程杀死
err := cmd.Run() //运行该命令
fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err) //输出进程ID、运行时间、错误
}

打开终端查看:发现进程能被kill掉

root@flight:~$ ps au
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
root 1804 0.0 0.0 4278124 996 s000 R+ 4:10下午 0:00.00 ps au
didi 1158 0.0 0.0 4296892 1284 s000 S 4:08下午 0:00.03 -bash
didi 1798 0.0 0.0 4270348 564 s001 S+ 4:10下午 0:00.00 sleep 5
root@flight:~$ ps au
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
root 1819 0.0 0.0 4268908 972 s000 R+ 4:10下午 0:00.00 ps au
didi 1158 0.0 0.0 4296892 1300 s000 S 4:08下午 0:00.03 -bash
root@flight:~$

当进程fork出子进程:

package main

import (
"fmt"
"os/exec"
"time"
) func main() {
cmd := exec.Command("/bin/sh", "-c", "watch date > date.txt") //watch进程fork出了其他子进程
start := time.Now() //记录启动时间
time.AfterFunc(3*time.Second, func() { cmd.Process.Kill() }) //3s后将此进程杀死
err := cmd.Run() //运行该命令
fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err) //输出进程ID、运行时间、错误
}

输出结果:发现同样运行了3s被kill

[root@localhost ~]# go run test.go
pid=16860 duration=3.001284491s err=signal: killed #同样运行了3s被kill
[root@localhost ~]#

但是查看用户进程会发现不一样:

[root@localhost ~]# ps -af
UID PID PPID C STIME TTY TIME CMD
root 2409 2277 0 17:07 pts/1 00:00:01 top
root 5118 4953 0 17:09 pts/3 00:00:00 top
root 16804 2269 14 17:14 pts/0 00:00:00 go run test.go
root 16855 16804 0 17:14 pts/0 00:00:00 /tmp/go-build276131739/b001/exe/test
root 16860 16855 0 17:14 pts/0 00:00:00 /bin/sh -c watch date > date.txt
root 16861 16860 0 17:14 pts/0 00:00:00 watch date
root 16954 4919 0 17:14 pts/2 00:00:00 ps -af
[root@localhost ~]#
[root@localhost ~]#
[root@localhost ~]# ps -af
UID PID PPID C STIME TTY TIME CMD
root 2409 2277 0 17:07 pts/1 00:00:01 top
root 5118 4953 0 17:09 pts/3 00:00:00 top
root 16861 1 0 17:14 pts/0 00:00:00 watch date
root 17169 4919 0 17:14 pts/2 00:00:00 ps -af

现象:

  • 程序运行3s后/bin/sh -c watch date > date.txt被kill,但是watch date依然存在。
  • watch date的父进程是1号进程。

问题原因

Go是使用kill(2)向sh进程的PID发了一个KILL信号,但没有发给watch进程,sh进程被kill之后,导致watch进程变成孤儿进程。

解决方案

kill(2)不但支持向单个PID发送信号,还可以向进程组发信号,我们只要把sh进程及其所有子进程放到一个进程组里,就可以批量Kill了。关键是PGID的设置,默认情况下,子进程会把自己的PGID设置成与父进程相同,所以,我们只要设置了sh进程的PGID,所有子进程也就相应的有了PGID。

注意:传递进程组PGID的时候要使用负数的形式。

注意:下面这种方式适合非su - <user> command执行命令的方式,否则杀死父进程后,子进程将被托管成为孤儿进程。因为他们的PGID不一样,即使设置了cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}也没有用。正确的方式是采用exec包自带的方式来指定执行用户

package main

import (
"fmt"
"os/exec"
"syscall"
"time"
) func main() {
cmd := exec.Command("/bin/sh", "-c", "watch date > date.txt") // Go会将PGID设置成与PID相同的值
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Setpgid=true start := time.Now()
time.AfterFunc(3*time.Second, func() { syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }) //想要杀死整个进程组,而不是单个进程,需要传递负数形式。
err := cmd.Run()
fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err)
}

如果想要指定用户:

sysuser, err := user.Lookup("user1") // 通过用户名来获取用户信息
if err != nil {
fmt.Println(err)
}
uid, err := strconv.Atoi(sysuser.Uid) // 将UID的类型转换成 uint32
if err != nil {
fmt.Println(err)
}
gid, err := strconv.Atoi(sysuser.Gid) // 将GID的类型转换成 uint32
if err != nil {
fmt.Println(err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
}

参考文档

链接:Go语言中Kill子进程的正确姿势

Go语言中Kill子进程的正确姿势的更多相关文章

  1. int转换char的正确姿势

    一:背景 在一个项目中,我需要修改一个全部由数字(0~9)组成的字符串的特定位置的特定数字,我采用的方式是先将字符串转换成字符数组,然后利用数组的位置来修改对应位置的值.代码开发完成之后,发现有乱码出 ...

  2. C语言中,头文件和源文件的关系(转)

    简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句, ...

  3. C语言中do...while(0)的妙用(转载)

    转载来自:C语言中do...while(0)的妙用,感谢分享. 在linux内核代码中,经常看到do...while(0)的宏,do...while(0)有很多作用,下面举出几个: 1.避免goto语 ...

  4. C语言中system()函数的用法总结(转)

    system()函数功能强大,很多人用却对它的原理知之甚少先看linux版system函数的源码: #include <sys/types.h> #include <sys/wait ...

  5. C语言中void*详解及应用

    void在英文中作为名词的解释为“空虚:空间:空隙”:而在C语言中,void被翻译为“无类型”,相应的void *为“无类型指针”.void似乎只有“注释”和限制程序的作用,当然,这里的“注释”不是为 ...

  6. (七)C语言中的void 和void 指针类型

    许多初学者对C中的void 和void 的指针类型不是很了解.因此常常在使用上出现一些错误,本文将告诉大家关于void 和void 指针类型的使用方法及技巧. 1.首先,我们来说说void 的含义: ...

  7. 转:C语言中的static变量和C++静态数据成员(static member)

    转自:C语言中的static变量和C++静态数据成员(static member) C语言中static的变量:1).static局部变量        a.静态局部变量在函数内定义,生存期为整个程序 ...

  8. C语言中.h和.c文件解析(很精彩)

    C语言中.h和.c文件解析(很精彩)   简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析 ...

  9. C语言中.h和.c文件解析

    整理自C语言中.h和.c文件解析(很精彩) Part.1(林锐<高质量C/C++编程>) 通过头文件来调用库功能.在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的 ...

  10. C/C++语言中const的用法

    1. const 在C和C++中的区别     C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中. 所 ...

随机推荐

  1. 2018-3-8-WPF-UncommonField-类型是什么

    title author date CreateTime categories WPF UncommonField 类型是什么 lindexi 2018-3-8 16:25:2 +0800 2018- ...

  2. ESP32 分区表

    当你编译程序,发现 app partition is too small for binary 错误的时候,就涉及到 ESP32 分区表的内容了. 一.基本概念 在了解分区之前,先了解一下以下概率,便 ...

  3. Solution Set - DP

    CF101E Candies and Stones Link&Submission. DP 的状态设计和转移都是显然的,唯一的问题在于需要输出方案,而这题卡空间.会发现如果用 bitset 存 ...

  4. 【YoloDeployCsharp】基于.NET Framework的YOLO深度学习模型部署测试平台

    1. 项目介绍    基于.NET Framework 4.8 开发的深度学习模型部署测试平台,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det.Seg.Pose.Obb ...

  5. 密码学—Vigenere破解Python程序

    文章目录 概要 预备知识点学习 整体流程 技术名词解释 技术细节 小结 代码 概要 破解Vigenere需要Kasiski测试法与重合指数法的理论基础 具体知识点细节看下面这两篇文章 预备知识点学习 ...

  6. 编译mmdetection3d时,无root权限下为虚拟环境单独创建CUDA版本

    在跑一些深度学习代码的时候,如果需要使用mmdetection3d框架,下载的pytorch的cudatoolkit最好需要和本机的cuda版本是一样的,即输入nvcc -V命令后显示的版本一样. 但 ...

  7. Fastapi获取其他第三方回调

    flask.django获取第三方回调数据,可以用request.data直接获取全部参数.而很多同学不知道fastapi如何获取回调的全部参数,其实可以通过request.body(). 如: @p ...

  8. Anaconda 虚拟环境

    1. 查看虚拟环境 conda env list 2. 创建虚拟环境 conda create -n env_name python=3.8 --- env_name: 虚拟环境名 --- pytho ...

  9. 视觉族: 基于Stable Diffusion的免费AI绘画图片生成器工具

    视觉族是一款基于Stable Diffusion文生图模型的免费在线AI绘画图片生成器工具,可以使用提示关键词快速生成精美的艺术图片,支持中文提示.无论你是想要创作自己的原创作品,还是想要为你的文字增 ...

  10. 『手撕Vue-CLI』拉取版本号

    开篇 在上一篇文章中,给 nue-cli 添加了与用户终端交互的功能,这一次来实现一个拉取版本号的功能. 这个功能的背景是,有时候我们在使用脚手架的时候,不同版本的脚手架可能会有不同的功能,所以用户有 ...