需求描述#
- 設置一個並發度
INS
,表示要開的進程數量 - 使用這
INS
個進程,計算從start
到end
之間的數字累加和 start
和end
通過getopt
解析命令行參數獲取
./a.out -s 12 -e 24
- 輸出一個整型結果:
sum
[注意]
- 主要涉及文件及進程相關操作
- 使用文件進行數據共享,需要考慮數據競爭(data race)
- 嘗試使用文件鎖來模擬線程之間的互斥鎖
- 通過文件鎖實現臨界數據(多個進程或線程競爭修改的數據)的同步訪問
- 需要學習 flock:man 2 flock
最終結果#
- 用 100 個進程算 1 到 1000 的累加和,效果截取如下:
- 成功讓進程們在同一塊數據上搶著計算累加和
實現過程#
思路流程圖#
- 把握好父進程和子進程的任務
- 關鍵:多進程訪問同一文件的加鎖操作,讓讀寫數據成為 “原子操作” [不可分割的最小單位]
- 可以理解為原子操作,但是本質上只是讓數據的讀寫過程完整
- 進程可能因為時間片用完而中斷,但因為鎖的存在,此時其它進程還無法訪問這些數據
獲取命令行參數#
捕獲 - s、-e 選項,使用該選項必須帶參數
#include "head.h"
int main(int argc, char **argv) {
int opt, start = 0, end = 0;
while ((opt = getopt(argc, argv, "s:e:")) != -1) {
switch (opt) {
case 's':
start = atoi(optarg); // atoi: 字符串->整數
break;
case 'e':
end = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -s start_num -e end_num\n", argv[0]);
exit(1);
}
}
printf("start = %d\nend = %d\n", start, end);
return 0;
}
- 頭文件 "head.h" 見末尾
- atoi:字符串👉整數,optarg 是字符數組
- 效果如下:
- 🆗
創建 INS 個進程#
使用 fork 創建 INS 個進程,注意使用 wait 防止殭屍進程的產生
#define INS 100
pid_t pid;
int x = 0; // x: 第幾號進程
for (int i = 1; i <= INS; i++) {
if ((pid = fork()) < 0) {
perror("fork");
exit(1); // 只是圖方便,工作中不如此操作
}
if (pid == 0) {
x = i; // 給子進程編號
break; // 關鍵,否則會不斷套娃
}
}
if (pid != 0) {
// 防止產生殭屍進程 [等待完所有的子進程]
for (int i = 1; i <= INS; i++) {
wait(NULL);
}
// 父進程
printf("I'm parent!\n");
} else {
printf("I'm %dth child!\n", x);
}
- 該段代碼放在主函數中獲取命令行參數後
- INS 定義為宏
- 子進程創建失敗直接 exit (1),是為了方便,工作中忌
- 效果如下:
- 成功創建 100 個子進程
基於文件的數據讀寫接口#
使用文件作為進程之間共享數據的載體
- 如何在文件中存儲數據?ASCII 碼 [字符]、int [低 16 位 + 高 16 位]...
- 這裡使用結構體存儲數據,結構清晰
- 存儲加數、和數
char data_file[] = "./.data";
char lock_file[] = "./.lock"; // [可選] 設置專門的一把鎖
// 要傳遞的數據
struct Msg {
int now; // 加數
int sum; // 和
};
struct Msg data; // 結構體數據
// 寫入結構體數據
size_t set_data(struct Msg *msg) {
FILE *f = fopen(data_file, "w"); // 寫
if (f == NULL) {
perror("fopen");
return -1; // 在一個小函數裡面exit過於粗魯
}
size_t nwrite = fwrite(msg, 1, sizeof(struct Msg), f); // 每次寫1個字節
fclose(f);
return nwrite; // 返回的是成功寫的字節數,如果出錯也返回給上層
}
// 讀取結構體數據
size_t get_data(struct Msg *msg) {
FILE *f = fopen(data_file, "r");
if (f == NULL) {
perror("fopen");
return -1;
}
size_t nread = fread(msg, 1, sizeof(struct Msg), f); // 讀入結構體數據到msg中
fclose(f);
return nread;
}
- 創建全局變量 data,用來在進程中操作數據
- 利用標準文件操作,底層文件操作也可行
- 返回值可供調用者檢查讀寫成功與否
加入鎖⭐#
讓進程搶著維護共享數據,並保護數據文件不被同時操作
【兩種思路】使用一個文件;使用兩個文件
- 思路一:直接對數據文件加鎖
char data_file[] = "./.data";
// 做加法 [原子操作:讀 + 寫];end:加法停止條件;id:孩子的編號 [可用上帝視角監控]
void do_add(int end, int id) {
// 孩子一直在裡面做加法
while (1) {
/*
* 思路一:一個文件,直接在數據文件加鎖
*/
// 打開data_file用來加鎖
FILE *f = fopen(data_file, "r");
// 加互斥鎖
flock(f->_fileno, LOCK_EX);
// 從文件讀取數據 [get_data函數裡會再次打開data_file文件,對應新的fd,鎖不共用]
if (get_data(&data) < 0) continue;
// 加數+1,並判斷加數是否超過範圍
if (++data.now > end) {
fclose(f);
break;
}
// 做加法
data.sum += data.now;
printf("The <%d>th Child : now = %d, sum = %d\n", id, data.now, data.sum);
// 將數據寫入文件
if (set_data(&data) < 0) continue;
// 解鎖 [後面關閉其實也會自動釋放鎖]
flock(fileno(f), LOCK_UN);
fclose(f);
}
}
- 函數參數:end 作為加法的停止條件參照,id 可用來觀察每次加法是哪個孩子算的
- 加鎖 👉 解鎖中間的過程就是原子操作[不可分割的最小單位]
- 封裝了讀數據、做計算、寫數據操作,過程中數據不會被搶占
- 由文件指針 FILE* f 獲得文件描述符 fd
- ① f->_fileno
- ② fileno(f)
- [PS]
- 重複打開一個文件會得到不同的文件描述符,鎖也相互獨立
- 文件關閉會自動釋放鎖
- 在每次調用讀寫接口後,利用好返回值判斷操作成功與否
- 思路二:設置專門的文件加鎖
char data_file[] = "./.data";
char lock_file[] = "./.lock"; // 設置專門的一把鎖
void do_add(int end, int id) {
while (1) {
/*
* 思路二:兩個文件,使用單獨的文件作為鎖 [更易理解]
*/
// 打開或創建一個鎖文件;如果文件已加鎖,將等待使用者解鎖
FILE *lock = fopen(lock_file, "w"); // "w":如果不存在文件,會創建一個
if (lock == NULL) {
perror("fopen");
exit(1);
}
// 加鎖
flock(lock->_fileno, LOCK_EX);
// 從文件讀取數據
if (get_data(&data) < 0) {
fclose(lock); // 關閉鎖文件,釋放鎖
continue;
}
// 加數+1,並判斷是否滿足停止加的條件
if (++data.now > end) {
fclose(lock);
break;
}
// 做加法
data.sum += data.now;
printf("The <%d>th Child : now = %d, sum = %d\n", id, data.now, data.sum);
// 將數據寫入文件
if (set_data(&data) < 0) continue;
// 解鎖
flock(lock->_fileno, LOCK_UN);
fclose(lock);
}
}
- lock_file 就是單純為了做鎖
- 效果如下:【單核,5 個進程,計算 1~100】
- 單核的效果比多核更整齊
- 單核一次只能運行一個進程
- 可以使用 usleep () 提前掛起進程,不讓一個進程計算那麼久,讓順序更亂
- 若將輸出傳給 more,它會將輸出按進程區分,重新排列展示
- 【注意】
- 在主函數中,將數據初始值先寫入文件,否則文件為空 [詳見完整代碼]
- 在主函數,子進程邏輯中調用 do_add () 函數即可,父進程邏輯中在等待所有子進程結束後從數據文件獲取並輸出最終結果即可
- ❗ 如果不加鎖,結果仍然對
- 加數和和數是包裝在一起,加法不會出錯
- 但每個進程都會完整地算一遍結果,可能是緩衝區的緣故?不是
- 在所有寫操作後都加入 fflush,儘管有一些接著算的情況,但是每個進程都還是會跑到最後的正確結果
- 相當於有進程算完了,數據寫入文件,但是另一個進程讀取的還不是最新的數據,還會自己再算一遍累加
- 解釋
- 多個進程打開同一文件,每個進程都有它自己的文件表項(file 對象),包含它自己的文件位移量
- 所以對於多個進程讀同一文件都能正確工作,但寫同一文件可能產生預期不到的結果,可參考使用 pread、pwrite
- 還可參考Linux 下多進程同時操作文件——cnblogs
完整代碼#
sum.c#
#include "head.h"
#define INS 100
char data_file[] = "./.data";
char lock_file[] = "./.lock"; // [可選] 設置專門的一把鎖
// 要傳遞的數據
struct Msg {
int now; // 加數
int sum; // 和
};
struct Msg data; // 結構體數據
// 寫入結構體數據
size_t set_data(struct Msg *msg) {
FILE *f = fopen(data_file, "w"); // 寫
if (f == NULL) {
perror("fopen");
return -1; // 在一個小函數裡面exit過於粗魯
}
size_t nwrite = fwrite(msg, 1, sizeof(struct Msg), f); // 每次寫1個字節
fclose(f);
return nwrite; // 返回的是成功寫的字節數,如果出錯也返回給上層
}
// 讀取結構體數據
size_t get_data(struct Msg *msg) {
FILE *f = fopen(data_file, "r");
if (f == NULL) {
perror("fopen");
return -1;
}
size_t nread = fread(msg, 1, sizeof(struct Msg), f); // 讀入結構體數據到msg中
return nread;
}
// 做加法 [原子操作:讀 + 寫];end:加法停止條件;id:孩子的編號 [可用上帝視角監控]
void do_add(int end, int id) {
// 孩子一直在裡面做加法
while (1) {
/*
* 思路二:兩個文件,使用單獨的文件作為鎖 [更易理解]
*/
// 打開或創建一個鎖文件;如果文件已加鎖,將等待使用者解鎖
FILE *lock = fopen(lock_file, "w"); // "w":如果不存在文件,會創建一個
if (lock == NULL) {
perror("fopen");
exit(1);
}
// 加鎖
flock(lock->_fileno, LOCK_EX);
// 從文件讀取數據
if (get_data(&data) < 0) {
fclose(lock); // 關閉鎖文件,釋放鎖
continue;
}
// 加數+1,並判斷是否滿足停止加的條件
if (++data.now > end) {
fclose(lock);
break;
}
// 做加法
data.sum += data.now;
printf("The <%d>th Child : now = %d, sum = %d\n", id, data.now, data.sum);
// 將數據寫入文件
if (set_data(&data) < 0) continue;
// 解鎖
flock(lock->_fileno, LOCK_UN);
fclose(lock);
/*
* 思路一:一個文件,直接在數據文件加鎖
*/
/*
// 打開data_file用來加鎖
FILE *f = fopen(data_file, "r");
// 加互斥鎖
flock(f->_fileno, LOCK_EX);
// 從文件讀取數據 [get_data函數裡會再次打開data_file文件,對應新的fd,鎖不共用]
if (get_data(&data) < 0) continue;
// 加數+1,並判斷加數是否超過範圍
if (++data.now > end) {
fclose(f);
break;
}
// 做加法
data.sum += data.now;
printf("The <%d>th Child : now = %d, sum = %d\n", id, data.now, data.sum);
// 將數據寫入文件
if (set_data(&data) < 0) continue;
// 解鎖 [後面關閉其實也會自動釋放鎖]
flock(fileno(f), LOCK_UN);
fclose(f);
*/
}
}
int main(int argc, char **argv) {
int opt, start = 0, end = 0;
while ((opt = getopt(argc, argv, "s:e:")) != -1) {
switch (opt) {
case 's':
start = atoi(optarg); // atoi: 字符串->整數
break;
case 'e':
end = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -s start_num -e end_num\n", argv[0]);
exit(1);
}
}
printf("start = %d\nend = %d\n", start, end);
// 先將初始數據寫入文件
if (set_data(&data) < 0) return -1; // data為全局變量,成員默認均為0
pid_t pid;
int x = 0; // x: 第幾號進程
for (int i = 1; i <= INS; i++) {
if ((pid = fork()) < 0) {
perror("fork");
exit(1); // 只是圖方便,工作中不如此操作
}
if (pid == 0) {
x = i; // 給子進程編號
break; // 關鍵,否則會不斷套娃
}
}
if (pid != 0) {
// 防止產生殭屍進程 [等待完所有的子進程]
for (int i = 1; i <= INS; i++) {
wait(NULL);
}
if (get_data(&data) < 0) return -1; // 獲得最終結果
printf("sum = %d\n", data.sum);
} else {
do_add(end, x); // 子進程唯一的事
}
return 0;
}
head.h#
#ifndef _HEAD_H
#define _HEAD_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/file.h>
#endif
- 可能有多餘的頭文件,不是重點
參考#
- 主要知識點參考《網絡與系統編程》
- 0 課程介紹及命令行解析函數——getopt
- 1 文件、目錄操作與實現 ls 的思路——fopen、fread、fwrite
- 3 多進程——fork、wait、flock⭐