跳转至

任务

任务的创建通常由一个系统调用触发,比如在 Windows 中,进程由 CreateProcess() 直接创建,并加载指定的可执行文件。在 UNIX/Linux 中,通常由 fork()clone() 系统调用创建一个副本(子进程),然后由 exec() 等一系列函数在子进程中加载新的程序

Linux 内核最初没有线程,2.6之后新增了 NPTL(Native POSIX Thread Library)线程库,但linux 的线程是用进程实现的,严格说, fork() 是进程操作,clone() 是线程操作

操作系统为新进程分配一个唯一的进程标识符(PID),用于追踪和管理进程。

然后为进程分配必要的资源,包括内存地址空间、文件句柄、设备等。

接着初始化一个与之相关的进程控制块(PCB),它包含了操作系统需要的关于进程的所有信息,如进程状态、程序计数器、寄存器、内存管理信息、账户信息、I/O状态信息等。

最后被置于就绪状态(Ready),等待被调度器分配 CPU 时间片运行。

进程组是一组相关联的进程的集合,通常由同一进程启动,并由进程组ID(PGID)进行标识。一个进程组也可以包含多个进程,一个进程可以属于多个进程组。

查看任务

# 列出所有任务
jobs -l
# 默认只显示任务编号(job number)
#   -l显示任务对应的PID
#   -r只列出放在后台运行的
#   -s只列出放在后台暂停的

# 显示任务ID
$!

# 将任务丢到后台执行,在命令的最后加上&
# 但如果有stdout和stderr依旧会输出到前台,导致前台被影响,可以利用数据重定向将输出传送到文件中
cp file1 file2 > /tmp/log.txt 2>&1 &

# 如果想要脱机后依旧运行任务则需要使用nohup
nohup command  # 前台运行
nohup command &  # 后台运行

查看进程

静态

# ps is process status
ps aux  # BSD风格(显示会截断)
ps -ef  # System V风格,标准格式
'
-e 显示所有进程,包括与终端无关的进程,也可以用:-A
-a 只显示与终端有关的所有进程
-u 显示所有与用户相关的进程,可以指定用户
-x 显示没有控制终端的进程,通常是后台进程或守护进程
-f 显示进程的详细信息
-l 显示长格式信息
-L 显示线程

UID          PID    PPID  C STIME TTY          TIME CMD
root       67583   67562  0 10:12 pts/1    00:00:00 -bash
root       67594   67583  0 10:12 pts/1    00:00:00 ps -f
'

动态

top

20220712184246

第1行 包含以下内容(也可以使用 uptime 单独查看))
  最近一次开机时长
  当前登了几个用户
  平均负载(满负载为1):1min,5min,15min,递增说明负载降低,递减说明系统越来越繁忙
第2行 任务统计
第3行 CPU状态:默认显示多个CPU的平均值,按1可单独显示
第4行 内存
第5行 交换分区(虚拟内存)

输入?号查看按键提示
    默认每3s更新一次,按s可以自定义间隔时间

其他命令

一台 Linux 机器上的进程总数目是有限制的。如果超过这个最大值,那么系统就无法创建出新的进程了

# 查看最大进程数
/proc/sys/kernel/pid_max  # 4194304kb

进程的优先级(PRI)由系统内核动态调整,用户可以通过nice值来影响它

PRI(new) = PRI(old) + nice(-20~19)

PRI越小优先级越高,非root用户nice可控范围在0~19,即只有调低优先级的权限

nice [-n num] command  # 进程运行前设置
renice [num] PID  # 进程运行中设置

任务管理

任务管理就是调用 kill 向进程发送一个信号

查看所有信号的编号和名字:kill -l

20210826205955

进程对信号的处理其实就包括两个问题:

  1. 进程如何发送信号:发送信号的系统调用是 kill()
  2. 进程收到信号后如何处理:signal() 它可以给信号注册 handler

内核中对不同的信号有不同的缺省行为,一般会采用退出(terminate),暂停(stop),忽略(ignore)这三种行为中的一种。

20210826212807

当我们运行 kill 1 这个命令的时候,希望把 SIGTERM 这个信号发送给 1 号进程,于是调用了kill()这个内核的调用接口(即系统调用),从而进入到了内核函数 sys_kill(),而内核在决定把信号发送给 1 号进程的时候,会调用 sig_task_ignored() 这个函数来做个判断,有以下三种情况:

  1. 缺省(Default),每个信号都有一个缺省行为,可使用man 7 signal查看,对于 SIGTERM 这个信号来说,它的缺省行为就是进程退出。
  2. 捕获(Catch),即进程可以在代码中针对某个信号,调用 signal() 注册相应的handler,这样进程在运行的时候,接收到指定信号,便不再使用 SIG_DFL 这个缺省的 handler,而是执行自定义的 handler。
  3. 忽略(Ignore),即不做任何处理

当 Linux 进程收到 SIGTERM 信号并且使进程退出,这时 Linux 内核对处理进程退出的入口点就是 do_exit() 函数,do_exit() 函数中会释放进程的相关资源,比如内存,文件句柄,信号量等等。

在做完这些工作之后,它会调用一个 exit_notify() 函数,用来通知和这个进程相关的父子进程等。

20210829153533

特权信号

有两个特权信号:SIGKILLSIGSTOP,它们是Linux 为 kernel 和超级用户去删除任意进程所保留的,不能被忽略,也不可以被捕获。

但对于初始化进程是可以忽略特权信号的,即 kill -9 1 是不会生效的

暂停任务

  • 20 SIGTSTP 温和地暂停,执行Ctrl+z时即发送的就是这个信号
  • 19 SIGSTOP 粗暴地暂停,不可屏蔽
kill -TSTP 进程编号
kill -STOP 进程编号
kill -CONT 进程编号  # 恢复进程

# 将任务暂并停放到后台
# 比如正在编辑一个文件或者执行的命令正在运行中时使用
Ctrl+z

# 让暂停到后台的任务在后台(background)继续运行
bg %job_id

# 将后台任务拿到前台(foreground)来执行
fg %job_id  # %可加可不加,为了与PID做区分建议加

终止任务

  • 15 SIGTERM 以正常的进程方式结束一个任务(默认)
  • 2 SIGINT 中断任务,完成善后工作再结束,执行Ctrl+c时即发送的就是这个信号
  • 3 SIGQUIT 中断任务,结束前执行core dump操作,不常用
  • 9 SIGKILL 立刻强制结束一个进程或任务(通常用来删除不正常的任务),不可屏蔽
  • 1 SIGHUP 启动被终止的进程或任务,重新读取一次配置文件
# 发送信号
kill -signal_id_or_name 进程号
killall -signal_id_or_name 进程名称
# kill 与 killall 的区别是一个用进程号一个用进程名称
# 避免误杀同名进程,推荐优先使用kill

kill %job_id  # 默认发送编号为15的SIGTERM信号
kill -9 %job_id  # 指定发送编号为9的信号
kill -SIGKILL %job_id  # 指定发送名称为SIGKILL的信号

任务状态

操作系统在切换任务时,需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。

这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。

20210828204948

运行态(R stat)

  • 运行中(即获得了CPU资源)
  • 运行队列中

睡眠态(等待队列)

进程需要等待某个资源,可以是一个信号量(Semaphore), 或者是磁盘I/O

  • TASK_INTERRUPTIBLE 可中断(S stat)
  • TASK_UNINTERRUPTIBLE 不可中断(D stat)

退出态

  • EXIT_ZOMBIE 进程退出前的状态(僵尸进程)
  • EXIT_DEAD 进程结束退出时的状态

每一个 Linux 进程在退出的时候都会进入一个僵尸状态(EXIT_ZOMBIE),实际上它只是一个等待被父进程删除掉的条目,而它不是一个真正的进程,所以无法被杀死,而是需要父进程调用 wait() 或者 waitpid() 系统调用来清理,这也是容器中 init 进程必须具备的一个功能

wait() 系统调用是一个阻塞的调用,也就是说,如果没有子进程是僵尸进程的话,这个调用就一直不会返回,那么整个进程就会被阻塞住,而不能去做别的事了
waitpid() 如果在调用的时候没有僵尸进程,那么函数就马上返回

任何进程在退出时都对应一个退出码,如果为0则表示成功或正常状态,非0则表示失败或异常状态,Shell 批处理时就是根据退出码来判断的

  • 僵尸进程

单一僵尸进程虽然无害,但如果有过多的僵尸进程,就会消耗系统中的进程数资源,最坏的情况是导致新的进程无法启动。

# 给僵尸进程的父进程发送消息,让其清理掉子进程
kill -s SIGCHLD ppid
# 如果以上命令无效,则需要杀死父进程,使其变为孤儿进程,继而被init进程回收
# 注意,如果僵尸进程的父进程是1,请谨慎删除,否则将会重启
sudo kill -9 ppid
  • 孤儿进程

如果某个进程死了,而它的子进程还没死,那么这些子进程就被形象地称之为孤儿,然后会被初始进程领养,即变为初始进程的直接子进程。