---
title: '信號 - 腳本 & 定時任務 crond'
disqus: kyleAlien
---
信號 - 腳本 & 定時任務 crond
===
## Overview of Content
啟動腳本的方式除了直接呼叫控制之外,還可以透過向腳本發送訊號來控制腳本
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**探索 Linux 訊號與後台進程管理:安排定期啟動腳本 | 運行時啟動腳本**](https://devtechascendancy.com/linux-signal-process-management/)
:::
[TOC]
## 認識 Linux 訊號
Linux 系統 & 應用程式可以產生很多信號,以下列出幾個我們最常見的信號
| 信號 | 對應值 | 說明 |
| - | - | - |
| 1 | SIGHUP | 掛起(Hang up) |
| 2 | SIGINT | 中斷(interrupt) |
| 3 | SIGQUIT | 停止 |
| 9 | SIGKILL | 無條件終止 |
| 15 | SIGTERM | 盡可能地終止 |
| 17 | SIGSTOP | 無條件停止(不等於終止) |
| 18 | SIGCON | 繼續停止的進程 |
| 19 | SIGTSTP | 暫停 |
```shell=
# 查看所有信號
man 7 signal
```
### Bash 處理信號
* 信號會傳遞到腳本中讓腳本去處理,而 Bash Shell 對於信號的處理有以下特性
* 默認情況下 Bash Shell 會處理 `SIGHUP`(1)、`SIGINT`(2)信號,但忽略 `SIGQUIT`(3)、`SIGTERM`(15)信號
* 如果 Shell 收到信號,那它會 **傳遞到每個 Sub Shell** (Child Shell) 中
### 產生信號:快捷鍵 & 指令
* 使用 **快捷鍵** 產生訊號
1. **中斷**:**`Ctrl + C` 產生 SIGINT(2)信號**;
測試如下:使用按鍵中斷 `sleep` 命令
```shell=
sleep 100
```
> 
2. **暫停**:**`Ctrl + Z` 產生 SIGTSTP(18)信號**;暫停進程與停止進程不同,暫停進程代表該進程仍在記憶體中,隨時可恢復
測試如下:使用按鍵暫停 `sleep` 命令
```shell=
sleep 100
```
> 暫停進程後,Linux 會分配一個作業好 (`job number`) 給暫停的進程
>
> 
:::info
* **可以透過 `ps f` 命令查看 `STAT` 欄位,來觀察進程的狀態**
> 
可以看到上圖 **進程 STAT 為 `T`,這代表進程(執行命令的進程)被停止**
> 
:::
* 使用 **指令** 產生訊號:
使用 `kill` 命令,並指定要產生的信號,就可以對進程發送指定信號;格式如下
```shell=
kill [options] <PID>
```
**可以透過 `-l` 查看 kill 可發送的訊號**
> 
* 範例:透過 kill 對指定進程(透過 PID)發送指定信號
```shell=
# 對進程 99882
# 發送 SIGKILL 信號
kill -9 99882
```
> 
### 捕捉信號 trap
* 我們知道每個腳本都預設可以接收訊號;我們也可以 **透過 `trap` 命令主動去指定我們要捕捉某個訊號**,`trap` 格式如下:
```shell=
trap <命令> 指定訊號
```
> 捕捉到指定訊號後,執行 `命令` 的內容
* **Trap 捕捉信號範例**:
捕捉但不處理,以下捕捉 `SIGINT` 訊號(使用 `Ctrl-C` 來產生這個訊號)
```shell=
#!/bin/bash
trap "echo 'Sorry! I have trapped Ctrl-C'" SIGINT
count=1
while [ $count -le 10 ] ; do
echo "Test times: $count"
sleep 1
(( count++ ))
done
```
> 
### 捕捉腳本退出信號
* **在腳本完成並準備退出時會發出一個 `EXIT` 訊號**
而我們也可以 **透過 `trap` 命令捕捉腳本退出的時機**(也就是腳本退出時發生的信號)… 範例如下:
```shell=
#!/bin/bash
trap "echo 'The script finish'" EXIT
count=1
while [ $count -le 10 ] ; do
echo "Test times: $count"
sleep 1
(( count++ ))
done
```
> 
:::info
* **這裡的 `trap` 捕捉的信號是退出的信號**,如果想改成 `捕捉到某個信號才退出`,那我們可以這樣修改如下
在補捉到信號時,去執行 `exit` 命令
```shell=
#!/bin/bash
trap "echo 'The script get SIGSTP~'; exit" SIGSTP
count=1
while [ $count -le 10 ] ; do
echo "Test times: $count"
sleep 1
(( count++ ))
done
```
> 
:::
### 改變捕捉後的行為
* 如果要改變捕捉信號的行為,只需要「再寫一次 `trap` 命令」 並依照需求修改為需要執行的命令,這樣就會產生「**後蓋前**」的行為(後面的 `trap` 命令覆蓋前面的 `trap` 命令)
使用範例如下:
```shell=
#!/bin/bash
trap "echo 'Sorry! I have trapped Ctrl-C'" SIGINT
count=1
while [ $count -le 5 ] ; do
echo "Test times: $count"
sleep 1
(( count++ ))
done
# 跳出循環時,再次覆蓋捕捉 SIGINT
trap "echo 'I modify trapped~~'; whoami" SIGINT
count=1
while [ $count -le 5 ] ; do
echo "~~ Second times: $count"
sleep 1
(( count++ ))
done
```
:::warning
* 這裡要注意,要捕捉同一個信號才可以覆蓋
:::
> 
### 移除捕捉到的信號
* 將 `trap` 命令,轉換為「單破折號(`-`)」或是「雙破折號(`--`)」就可以取消捕捉;
格式如下,兩種方式都可以,擇一即可
```shell=
trap - 指定信號
trap -- 指定信號
```
移除 `trap` 信號的範例:
```shell=
#!/bin/bash
trap "echo 'Sorry! I have trapped Ctrl-C'" SIGINT
count=1
while [ $count -le 5 ] ; do
echo "Test times: $count"
sleep 1
(( count++ ))
done
# 移除信號
trap -- SIGINT
count=1
while [ $count -le 5 ] ; do
echo "~~ Second times: $count"
sleep 1
(( count++ ))
done
```
> 
## 後台進程與腳本
腳本在後台(`background`)模式中就不會影響前台,前台可以繼續作業
### 腳本後台運行:JobIndex
* 要讓腳本運行在後台很簡單,只需要在呼叫腳本時,在腳本後添加 `&` 符號即可;格式如下
```shell=
運行腳本 &
```
**腳本後台運行範例**
```shell=
#!/bin/bash
count=1
while [ $count -le 10 ] ; do
echo "Test times: $count"
sleep 1
(( count++ ))
done
```
> 
:::warning
* 腳本在後台運行時仍會使用 `STDOUT`、`STDERR`,如果你沒有重新指向的話,它就會輸出在螢幕上
> 
:::
* 在規定腳本後台運行後,會 **返回一組號碼,那組號碼就是後台運行的 `JobIndex` & `PID`**
> Job 概念後面會提及
>
> 
:::danger
在終端啟動的腳本後台腳本會與當前終端產生關聯,**如果終端關閉了,那該終端啟動的後台進程也會關閉**
:::
### 腳本脫離終端關聯:nohup
* 前面有提到,**腳本預設會與啟動它的終端有聯繫,如果終端關閉,該腳本就算沒運行完畢也會被關閉**;以下範例,我們啟動一個腳本,關閉視窗(中斷)
1. 撰寫測試腳本:該腳本重新定向輸出到 `relate_termial` 檔案
```shell=
#!/bin/bash
count=1
while [ $count -le 10 ] ; do
echo "Hello, relate with termial: $count"
sleep 1
(( count++ ))
done
```
2. 背景啟動以下腳本
```shell=
./normal_output.sh &
```
3. 快速關閉啟動腳本的終端(按下叉叉),輸出自然被中斷
* **Linux 有提供一個 `nohup` 命令,可以 ^1.^切斷腳本 & 啟動它的終端的關聯,如果有輸出結果 ^2.^ 將結果輸出到 `nohup.out` 文件中**
:::info
如果在同級目錄中多個 `nohup` 命令,那輸出結果全部都會匯集到 `nohup.out` 文件(容易混亂)
:::
使用的腳本跟上面相同,不過在啟動腳本時改變啟動命令
1. 啟動命令改動如下
```shell=
nohup ./normal_output.sh &
```
> 
2. 查看 `nohup.out` 檔案:可以發現輸出結果到指定檔案,關閉終端不對輸出產生影響
> 
:::warning
* 如果你在腳本中使用 `exec` 另起一個進程輸出結果到檔案,那終端仍與 `exec` 啟動的進程有關聯,`nohup` 這時就無法中斷兩者(exec & 終端)之間的關聯
:::
## 後台進程管理
管理進程可以使用 `kill` 命令對不同進程(指定 PID)來發送不同的信號,除了 `kill` 命令之外還有另一個常用的 `jobs` 命令
### Jobs 查看後台進程
接下來使用這個腳本在背景運行,進行測試
```shell=
#!/bin/bash
count=1
while [ $count -le 10 ] ; do
sleep 10
(( count++ ))
done
```
* `jobs` 命令可以查看當前 Shell 正在處理的作業,並且它有幾個常用的 options,如下表
| Options | 說明 |
| -------- | -------- |
| -l | 列出進程的 PID & job 號 |
| -n | 列出上次 Shell 發出的通知後改變的狀態 |
| -p | 只列出 PID |
| -r | 列出運行中的作業 |
| -s | 列出已停止的作業 |
:::success
* **jobs 列出的 `+`、`-` 號**
**`+` 號代表默認作業,`-` 下一個會被指定的作業**;**在控制流程中,如果沒有指定作業號** 就會用 `+`、`-` 號 依序處理
:::
* **背景運行腳本,並使用 `jobs` 命令查看任務**
> 
* **測試多背景作業下:符號 `+`、`-` 號的改變**
1. 啟動多個背景腳本,並使用 `jobs -l` 查看,可以看到`+`、`-` 號只會有一組
> 
2. 使用 `kill` 命令對當前是 `+` 號的進程發出 `SIGKILL` 命令,會發現 `+`、`-` 號的轉移
> 
### 在背景啟動暫停的作業:bg
* **`bg` 命令**:該令令可以將暫停中的作業緩醒,並將該作業歸納到後台運行,`bg` 命令格式如下
```shell=
bg [job 編號]
## 如果沒有指定,則依照 +- 號順序
bg
```
1. 啟動腳本
```shell=
./job_3.sh
```
2. 暫停腳本使用 `Ctrl + Z` 快捷鍵,對腳本發出暫停信號(`SIGTSTP`)
> 可以發現腳本被暫停 (`Stopped`)
3. 使用 `bg` 命令,來將暫停任務喚醒,並歸納到後台運行,再使用 `jobs` 查看任務是否真的在後台運行
```shell=
./job_3.sh
```
> 
### 將背景暫停作業切換到前景:fg
* **`fg` 命令**:將在暫停中的作業,切換至前景(與使用者交互的進程)繼續運行;指令格式如下
```shell=
jg [job 編號]
## 如果沒有指定,則依照 +- 號順序
jg
```
範例如下
1. 啟動耗時 Shell
```shell=
./job_3.sh
```
2. 使用 `Ctrl + Z` 對腳本發出暫停訊號
3. 透過 `jobs -l` 查看當前 Shell 運行的相關任務
```shell=
./jobs -l
```
4. 使用 `fg` 指令將其切換至當前交互介面繼續訓行
> 
## 進程謙讓度
系統會根據進程的 **謙讓度 來決定 CPU 資源的分配**,如果謙讓度越低則分配的的資源越高,反知則越低
:::info
* **謙讓度 相關知識**
* 範圍是 `-20`(資源分配高) ~ `19`(資源分配低),值越小資源越多
* 使用者可以自由調高進程的謙讓度(數字提高),但是 **只有 root 使用者可以調低謙讓度(數字降低)**
:::
### 調整謙讓度:nice / renice
* `nice`、`renice` 兩個指令都可以調整腳本的謙讓度,**兩者個差別在於 `nice` 是腳本尚未啟動時就設定,而 `renice` 則是腳本啟動後再做調整**
* **`nice` 命令範例**
| Options | 功能 |
| -------- | -------- |
| `-n`, `--adjustment` | 調整謙讓度(預設為 10) |
```shell=
# 背景啟動耗時任務,並調整謙讓度到 10
nice -n 10 ./job_3.sh &
# 透過 ps 指令查看謙讓度... 等等訊息
ps -p 120437 -o pid,ppid,ni,cmd
```
> 
* **`renice` 命令範例**
> `renice` 只能調整屬於自身的子進程謙讓度
| Options | 功能 |
| -------- | -------- |
| `-n`, `--adjustment` | 調整謙讓度(預設為 10) |
| `-p`, `--pid` | 指定 PID |
```shell=
# 背景啟動耗時任務,並調整謙讓度到 10
nice -n 10 ./job_3.sh &
# 透過 ps 指令查看謙讓度... 等等訊息
ps -p 120506 -o pid,ppid,ni,cmd
# 指定進程 ID,並調整謙讓度到 19
renice -n 19 -p 120506
# 再次檢查謙讓度... 等等訊息
ps -p 120506 -o pid,ppid,ni,cmd
```
> 
:::warning
* 再次提醒,要調低謙讓度(使用更多 CPU 資源)必須使用 root 用戶(或是 sudo 暫時調高權限)
:::
## 定時運行任務
Linux 系統可以透過特定命令來在固定(規劃好)的時間幫我們執行某些腳本
### 計畫執行:at 介紹
:::info
* **`at` 命令**
有些 Linux 發行版中會沒有內建 `at` 命令,請透過以下命令安裝
```shell=
sudo apt install -y at
```
:::
* **`at` 命令**:它會啟動一個守護進程 `atd` 在背景每 60s 進行檢查文件作業,而檢查的文件就是使用者排定的作業相關資訊
:::success
* **任務訊息文件所在位置**
> `atd` 維護的文件通常位於 `/var/spool/cron/atjobs` 目錄之下
>
> 
:::
* **`at` 命令格式如下**:
```shell=
# timespec 用來指定作業時間
at [Options] timespec
```
| Options | 說明 |
| -------- | -------- |
| -f | 用來指定腳本文件 |
| -q | 指定隊列(A to Z, a to z),對列則會影響到執行的優先級|
| -M | 永遠不發送 Mail 給使用者 |
* 如果你指定的 `timespec` 已經過了, 那它會在隔天的同個時段再去執行任務;而 `timespec` 有多種指定格式,以下列出幾種,詳細請看文件
1. **小時 & 分鐘**
> timespec 設定為 `17:55`
2. **AM / PM 指示**
> timespec 設定為 `05:55pm`
3. **特殊時間命名**:`now`, `noon`, `midnight`, `teatime`
> timespec 設定為 `teatime`
4. **標準日期格式**:`月日年`、`月/日/年`、`月.日.年`
> timespec 設定為 `10/10/23`
5. **文本日期**:加不加年都可以
> timespec 設定為 `Dec 25`
6. **時間基礎運算 `+` 號**
> timespec 設定為 `4pm + 3 days`
>
> timespec 設定為 `10:15AM + 7 days`
### 計畫執行:at 使用、相關命令
* **`at` 執行任務時的輸出**
`at` 命令的 `STDOUT`、`STDERR` **預設會指向作業用戶的電子郵件**,所以建議在使用 `at` 規劃時段時,最好將腳本內的指令重新導向
```shell=
#!/bin/bash
echo "Hello at."
sleep 3
echo "Finish at."
```
> 可以看到 **如果正常的腳本透過 `at` 執行,`STDOUT` 並不會指向螢幕**
>
> 
:::info
* **`at` 命令也可以在互動式**;結束設定時使用 `ctrl + D`
```shell=
at 12:00
echo "吃飯囉~"
```
> 
:::
* **`atq` 查看等待的任務隊列**
```shell=
at -f ./at_1.sh tomorrow
at -M -f ./at_1.sh 12:30
at -M -f ./at_1.sh teatime
at -M -f ./at_1.sh now
```
> 
* **`atrm` 移除排定作業**
> 只能刪除自己提交的作業,無法刪除其他人提交的作業
```shell=
atq
# 移除任務列表第二個任務
atrm 2
# 移除任務列表第三個任務
atrm 3
atq
```
> 
### 定期執行腳本:cron 介紹
* **`cron` 跟 `at` 的差異**:**`cron` 會定期執行,`at` 則是單次執行**
* **`cron` 命令特性如下**
1. **`cron` 時間表**:時間指定格式如下
```shell=
min hour dayofmonth month dayofweek command
```
cron 允許時間使用特定值、範圍取值(eg. `1~5`)、通配符... 等等,`cron` 的格式範例如下所示:
- 每天 `10:15` 執行某個任務(指令)
```shell=
15 10 * * * command
```
- 每週三 `10:15` 執行某個任務(指令)
> 星期日(0)~ 星期六(6)
```shell=
15 10 * * 3 command
```
- 每月 5 號 `10:15` 執行某個任務(指令)
> 月初(1)~ 月底(31)
```shell=
15 10 5 * * command
```
2. **任務指定**:任務必需要使用「**全路徑**」指定
以下指令的含意是,每天 `10:15` 分執行 `cron_1.sh` 腳本
```shell=
15 10 * * * /home/alien/Desktop/shell/chapter_16/cron_1.sh
```
3. **查看 `cron` 任務文件**:它會放在 `/etc` 目錄下,並以 `cron` 開頭取名
```shell=
ls -laF /etc/cron.*ly
```
> 
### 定期執行腳本:crontab 使用
:::warning
**只有系統管理員可以直接使用 `cron` 命令,一般使用者想使用,就可以透過 `crontab` 命令操作**(當然系統管理員也可以使用 `crontab` 命令)
:::
* **使用者 `crontab`檔案**:**編輯設定 crontab 任務**
:::info
* 每個使用者都可以有自己的 `crontab` 檔案,通常存在 `/var/spool/cron/crontabs` 目錄下
```shell=
sudo ls -laF /var/spool/cron/crontabs
```
> 
:::
1. 使用 `crontab -e` 編輯任務
```shell=
# 指定使用的編輯系統
export=vim
# 編輯任務
crontab -e
```
2. 輸入 cron 規範格式去指定任務
```shell=
15 10 * * * /home/alien/Desktop/shell/chapter_16/cron_1.sh
```
> 
:::danger
* 如果需要刪除某個任務,也必須透過 `contab -e` 編輯
:::
* **使用 `crontab -l` 命令**:查看 `cron` 任務的時間表
```shell=
crontab -l
```
> 
* **系統 corntab 檔案**:
系統的 `corntab` 檔案通常存在 `/etc/crontab` 檔案中,用它來安排系統任務的執行
> 
從上圖可以看到每天 `daily` 會透過 `run-parts` 命令來運行 `/etc/cron.daily` 目錄下的所有命令(順序運行)
> 
### 補足 cron 缺失:anacron 命令
:::info
* **`anacron` 命令**
有些 Linux 發行版中會沒有內建 `anacron` 命令,請透過以下命令安裝
```shell=
sudo apt install -y anacron
```
:::
* 如果 `cron` 在定時時間啟動任務但電源沒開,那在 **電源啟動後 `cron` 也不會去重新執行**
* **這時就可以用 `anacron` 命令,它會盡快運行錯過時間的作業項目**
1. `anacorn` 只會處理 `cron` 目錄下的程序,它有自己的文件去記錄每個指令應該運行的時機
```shell=
ls -laF /var/spool/anacorn
```
> 
2. `anacorn` 有自己的時間表,來檢查作業目錄
```shell=
sudo cat /etc/anacrontab
```
> 
## 啟動 Shell 即加載
請先複習 [**Shell 加載順序**](https://hackmd.io/tJkxCeTpQuK7TqVmn1PHVw?view#Shell-amp-%E7%92%B0%E5%A2%83%E8%AE%8A%E9%87%8F1) 這個章節,透過使用者啟動 Shell 時會加載的文件順序來把我們需要的指令(或腳本)加入
### 腳本插入 `.bashrc`
* 基本上除了最初在登入系統時一定會加載的 `/etc/profile` 文件之外,其他 **以下文件都不一定會加載**(每個發行版可能加載的文件都不同)
* `.bash_profile`
* `.profile`
* `.bash_login`
但基本上這些文件最終基本上都會加載(呼叫)到 `.bashrc` 文件,所以我們只要編輯該文件,就可以在每個 bash 中插入指令
1. **透過 vim `.bashrc`**
```shell=
vim .bashrc
```
2. **插入一行簡單指令**,保存並退出
```shell=
echo "Hello Bash"
```
3. **重新啟動一個新 bash 介面** 就會發現指令被輸出了~
> 
## Appendix & FAQ
:::info
:::
###### tags: `Linux Shell`