全局變量的異步I/O問題同樣屬于時序競態問題,其本質就是多個進程或者同一個進程中的多個時序(如主控程序和信號捕捉時的用戶處理函數)對同一個變量進行修改時,它們的執行順序不一樣就會導致該變量最終的值不一樣,從而產生不一樣的結果。
多個進程或者同一個進程中的多個時序對同一個變量進行操作時,應該盡量避免使用這種變量。在編程時也應當盡量避免使用全局變量。如果非用不可,則必須考慮該全局變量的使用順序問題,可以采用加鎖的方法對全局變量進行訪問。如果加鎖的方式無法解決,則直接就不訪問該變量,直到等待其它進程或時序訪問完之后才進行訪問,總之確保變量正確的訪問順序。
//分析如下父子進程交替數數程序,重點分析程序中3個sleep函數的作用,如果取消掉用戶處理函數中的兩個sleep函數會發生什么問題
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>int n = 0, flag = 0; //定義兩個全局變量(注意了)void sys_err(char *str)
{perror(str);exit(1);
}void do_sig_child(int num) //子進程的用戶處理函數
{printf("I am child %d\t%d\n", getpid(), n);n += 2;flag = 1; //對全局變量的修改sleep(1);
}void do_sig_parent(int num) //父進程的用戶處理函數
{printf("I am parent %d\t%d\n", getpid(), n);n += 2;flag = 1; //對全局變量的修改sleep(1);
}int main(void)
{pid_t pid;struct sigaction act;if ((pid = fork()) < 0)sys_err("fork");else if (pid > 0) {n = 1; //父進程從1開始數sleep(1); //父進程睡眠1s確保在父進程向子進程發信號之前,子進程完成了對信號的注冊act.sa_handler = do_sig_parent;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGUSR2, &act, NULL); //注冊自己的信號捕捉函數,父進程使用SIGUSR2信號do_sig_parent(0); //父進程先進行數數,從1開始while(1) {/* wait for signal */;if (flag == 1) { //父進程數數完成kill(pid, SIGUSR1);flag = 0; //標志已經給子進程發送完信號}}} else if (pid == 0){n = 2; //子進程從2開始數act.sa_handler = do_sig_child;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGUSR1, &act, NULL);while(1) {/* wait for signal */;if (flag == 1) {kill(getppid(), SIGUSR2);flag = 0;}}}return 0;
}
SIGUSR1和SIGUSR2信號。用戶自定義信號,程序員可以在程序中定義并使用該信號。默認動作為終止進程。
上述函數的正常執行結果本應該是:父進程數1、3、5、7、······;子進程數2、4、6、8、·······,且它們之間交替數數。
父進程中的第一個sleep函數確保在父進程向子進程發送信號前,子進程已經完成了對信號的注冊(因為子進程有可能失去CPU時間太長而未完成對信號的注冊),否則會導致子進程收到信號被終結。
在父進程中的全局變量flag在用戶處理函數中和主控程序(while循環中)都會被修改(子進程也一樣),但是正確的執行順序必須是:父進程完成數數→用戶處理函數置flag為1→父進程發信號→主控程序置flag為0。flag為1確保向進程發送信號,flag為0確保信號只是發送一次,不重復發送。但是,如果其中某一個進程(父進程或子進程)在while循環中剛發送完信號就失去了CPU,還未對flag進行修改,此時另一個進程處理完信號后,再次向該進程發送信號,此時該進程接收到信號不會接著執行flag=0的操作了,會馬上去處理信號,信號處理完后,才會回到主控程序執行flag=0的操作,此時顯然順序發生了顛倒,導致最終flag錯誤置為0。因此,該進程處理完信號后再也不會發送信號了,另一個進程也再也不會收到信號,從而更不會再發信號。兩個進程都在while循環中重復判斷條件,但是條件永遠不滿足。因此,用戶捕捉函數中的兩個sleep函數的作用就是確保,一個進程在向兩一個進程發送信號前,另一個進程主控程序中的flag=0的操作已經執行了,確保變量值修改的正確性。
如何解決該問題呢?可以使用后續章節講到的“鎖”機制。當操作全局變量的時候,通過加鎖、解鎖來解決該問題(互斥訪問,進程同步)。
現階段,我們在編程期間如若使用全局變量,應在主觀上注意全局變量的異步IO可能造成的問題。
上述問題雖然可以通過sleep函數來解決,但是sleep函數會導致數數效率太低,可以取消全局變量flag,讓發送信號的操作在用戶處理函數中完成即可。
//程序的修改和優化
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>int n = 0;
pid_t pid;void sys_err(char *str)
{perror(str);exit(1);
}void do_sig_child(int num)
{printf("I am child %d\t%d\n", getpid(), n);n += 2;kill(getppid( ) , SIGUSR2);
}void do_sig_parent(int num)
{printf("I am parent %d\t%d\n", getpid(), n);n += 2;kill(pid , SIGUSR1);
}int main(void)
{struct sigaction act;if ((pid = fork()) < 0)sys_err("fork");else if (pid > 0) {n = 1;sleep(1);act.sa_handler = do_sig_parent;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGUSR2, &act, NULL); do_sig_parent(0);while(1) {;}} else if (pid == 0){n = 2;act.sa_handler = do_sig_child;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGUSR1, &act, NULL);while(1) {;}}return 0;
}
?