--- 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 ``` > ![](https://hackmd.io/_uploads/BJySbF_42.png) 2. **暫停**:**`Ctrl + Z` 產生 SIGTSTP(18)信號**;暫停進程與停止進程不同,暫停進程代表該進程仍在記憶體中,隨時可恢復 測試如下:使用按鍵暫停 `sleep` 命令 ```shell= sleep 100 ``` > 暫停進程後,Linux 會分配一個作業好 (`job number`) 給暫停的進程 > > ![](https://hackmd.io/_uploads/Bk7lGYO42.png) :::info * **可以透過 `ps f` 命令查看 `STAT` 欄位,來觀察進程的狀態** > ![](https://hackmd.io/_uploads/S1kXmKON2.png) 可以看到上圖 **進程 STAT 為 `T`,這代表進程(執行命令的進程)被停止** > ![](https://hackmd.io/_uploads/rJhdXt_N2.png) ::: * 使用 **指令** 產生訊號: 使用 `kill` 命令,並指定要產生的信號,就可以對進程發送指定信號;格式如下 ```shell= kill [options] <PID> ``` **可以透過 `-l` 查看 kill 可發送的訊號** > ![](https://hackmd.io/_uploads/Bk8irtd4h.png) * 範例:透過 kill 對指定進程(透過 PID)發送指定信號 ```shell= # 對進程 99882 # 發送 SIGKILL 信號 kill -9 99882 ``` > ![](https://hackmd.io/_uploads/BkUgUFuVn.png) ### 捕捉信號 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 ``` > ![reference link](https://hackmd.io/_uploads/SJT5W-tVn.png) ### 捕捉腳本退出信號 * **在腳本完成並準備退出時會發出一個 `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 ``` > ![reference link](https://hackmd.io/_uploads/HkwUrWFNh.png) :::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 ``` > ![](https://hackmd.io/_uploads/Bkgo8bK42.png) ::: ### 改變捕捉後的行為 * 如果要改變捕捉信號的行為,只需要「再寫一次 `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 * 這裡要注意,要捕捉同一個信號才可以覆蓋 ::: > ![](https://hackmd.io/_uploads/r1OmNXq42.png) ### 移除捕捉到的信號 * 將 `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 ``` > ![](https://hackmd.io/_uploads/HkkG8X5En.png) ## 後台進程與腳本 腳本在後台(`background`)模式中就不會影響前台,前台可以繼續作業 ### 腳本後台運行:JobIndex * 要讓腳本運行在後台很簡單,只需要在呼叫腳本時,在腳本後添加 `&` 符號即可;格式如下 ```shell= 運行腳本 & ``` **腳本後台運行範例** ```shell= #!/bin/bash count=1 while [ $count -le 10 ] ; do echo "Test times: $count" sleep 1 (( count++ )) done ``` > ![](https://hackmd.io/_uploads/ByJScm9En.png) :::warning * 腳本在後台運行時仍會使用 `STDOUT`、`STDERR`,如果你沒有重新指向的話,它就會輸出在螢幕上 > ![](https://hackmd.io/_uploads/BJiz5Xq43.png) ::: * 在規定腳本後台運行後,會 **返回一組號碼,那組號碼就是後台運行的 `JobIndex` & `PID`** > Job 概念後面會提及 > > ![](https://hackmd.io/_uploads/HJajomcE2.png) :::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 & ``` > ![](https://hackmd.io/_uploads/ryncbEqN3.png) 2. 查看 `nohup.out` 檔案:可以發現輸出結果到指定檔案,關閉終端不對輸出產生影響 > ![](https://hackmd.io/_uploads/SJ0FMNqVn.png) :::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` 命令查看任務** > ![](https://hackmd.io/_uploads/Bk4buE9E2.png) * **測試多背景作業下:符號 `+`、`-` 號的改變** 1. 啟動多個背景腳本,並使用 `jobs -l` 查看,可以看到`+`、`-` 號只會有一組 > ![](https://hackmd.io/_uploads/S1QccV9En.png) 2. 使用 `kill` 命令對當前是 `+` 號的進程發出 `SIGKILL` 命令,會發現 `+`、`-` 號的轉移 > ![](https://hackmd.io/_uploads/SJoa5Nc42.png) ### 在背景啟動暫停的作業:bg * **`bg` 命令**:該令令可以將暫停中的作業緩醒,並將該作業歸納到後台運行,`bg` 命令格式如下 ```shell= bg [job 編號] ## 如果沒有指定,則依照 +- 號順序 bg ``` 1. 啟動腳本 ```shell= ./job_3.sh ``` 2. 暫停腳本使用 `Ctrl + Z` 快捷鍵,對腳本發出暫停信號(`SIGTSTP`) > 可以發現腳本被暫停 (`Stopped`) 3. 使用 `bg` 命令,來將暫停任務喚醒,並歸納到後台運行,再使用 `jobs` 查看任務是否真的在後台運行 ```shell= ./job_3.sh ``` > ![](https://hackmd.io/_uploads/ryQQaVcN2.png) ### 將背景暫停作業切換到前景:fg * **`fg` 命令**:將在暫停中的作業,切換至前景(與使用者交互的進程)繼續運行;指令格式如下 ```shell= jg [job 編號] ## 如果沒有指定,則依照 +- 號順序 jg ``` 範例如下 1. 啟動耗時 Shell ```shell= ./job_3.sh ``` 2. 使用 `Ctrl + Z` 對腳本發出暫停訊號 3. 透過 `jobs -l` 查看當前 Shell 運行的相關任務 ```shell= ./jobs -l ``` 4. 使用 `fg` 指令將其切換至當前交互介面繼續訓行 > ![](https://hackmd.io/_uploads/HkSOdbkB3.png) ## 進程謙讓度 系統會根據進程的 **謙讓度 來決定 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 ``` > ![](https://hackmd.io/_uploads/H1GXaWJS2.png) * **`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 ``` > ![](https://hackmd.io/_uploads/HkxhAZJS2.png) :::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` 目錄之下 > > ![](https://hackmd.io/_uploads/SyW4CfkH3.png) ::: * **`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` 並不會指向螢幕** > > ![](https://hackmd.io/_uploads/B11Dtzkr2.png) :::info * **`at` 命令也可以在互動式**;結束設定時使用 `ctrl + D` ```shell= at 12:00 echo "吃飯囉~" ``` > ![](https://hackmd.io/_uploads/HJOrTZrK3.png) ::: * **`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 ``` > ![](https://hackmd.io/_uploads/Hk_acGyr3.png) * **`atrm` 移除排定作業** > 只能刪除自己提交的作業,無法刪除其他人提交的作業 ```shell= atq # 移除任務列表第二個任務 atrm 2 # 移除任務列表第三個任務 atrm 3 atq ``` > ![](https://hackmd.io/_uploads/SJA-2M1rn.png) ### 定期執行腳本: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 ``` > ![](https://hackmd.io/_uploads/SJvJfQkSh.png) ### 定期執行腳本:crontab 使用 :::warning **只有系統管理員可以直接使用 `cron` 命令,一般使用者想使用,就可以透過 `crontab` 命令操作**(當然系統管理員也可以使用 `crontab` 命令) ::: * **使用者 `crontab`檔案**:**編輯設定 crontab 任務** :::info * 每個使用者都可以有自己的 `crontab` 檔案,通常存在 `/var/spool/cron/crontabs` 目錄下 ```shell= sudo ls -laF /var/spool/cron/crontabs ``` > ![](https://hackmd.io/_uploads/BkUUs-HK2.png) ::: 1. 使用 `crontab -e` 編輯任務 ```shell= # 指定使用的編輯系統 export=vim # 編輯任務 crontab -e ``` 2. 輸入 cron 規範格式去指定任務 ```shell= 15 10 * * * /home/alien/Desktop/shell/chapter_16/cron_1.sh ``` > ![](https://hackmd.io/_uploads/BJDKrmkSh.png) :::danger * 如果需要刪除某個任務,也必須透過 `contab -e` 編輯 ::: * **使用 `crontab -l` 命令**:查看 `cron` 任務的時間表 ```shell= crontab -l ``` > ![](https://hackmd.io/_uploads/SJUz8XJBh.png) * **系統 corntab 檔案**: 系統的 `corntab` 檔案通常存在 `/etc/crontab` 檔案中,用它來安排系統任務的執行 > ![](https://hackmd.io/_uploads/HykE3bBY3.png) 從上圖可以看到每天 `daily` 會透過 `run-parts` 命令來運行 `/etc/cron.daily` 目錄下的所有命令(順序運行) > ![](https://hackmd.io/_uploads/Hy4UhWrK2.png) ### 補足 cron 缺失:anacron 命令 :::info * **`anacron` 命令** 有些 Linux 發行版中會沒有內建 `anacron` 命令,請透過以下命令安裝 ```shell= sudo apt install -y anacron ``` ::: * 如果 `cron` 在定時時間啟動任務但電源沒開,那在 **電源啟動後 `cron` 也不會去重新執行** * **這時就可以用 `anacron` 命令,它會盡快運行錯過時間的作業項目** 1. `anacorn` 只會處理 `cron` 目錄下的程序,它有自己的文件去記錄每個指令應該運行的時機 ```shell= ls -laF /var/spool/anacorn ``` > ![reference link](https://hackmd.io/_uploads/Bkac_7kS3.png) 2. `anacorn` 有自己的時間表,來檢查作業目錄 ```shell= sudo cat /etc/anacrontab ``` > ![](https://hackmd.io/_uploads/H1NZFQySn.png) ## 啟動 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 介面** 就會發現指令被輸出了~ > ![](https://hackmd.io/_uploads/rJUhh7kSn.png) ## Appendix & FAQ :::info ::: ###### tags: `Linux Shell`