全局变量的异步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;
}