全局变量的异步I/O问题

全局变量的异步I/O问题同样属于时序竞态问题,其本质就是多个进程或者同一个进程中的多个时序(如主控程序和信号捕捉时的用户处理函数)对同一个变量进行修改时,它们的执行顺序不一样就会导致该变量最终的值不一样,从而产生不一样的结果。

多个进程或者同一个进程中的多个时序对同一个变量进行操作时,应该尽量避免使用这种变量。在编程时也应当尽量避免使用全局变量。如果非用不可,则必须考虑该全局变量的使用顺序问题,可以采用加锁的方法对全局变量进行访问。如果加锁的方式无法解决,则直接就不访问该变量,直到等待其它进程或时序访问完之后才进行访问,总之确保变量正确的访问顺序。

//分析如下父子进程交替数数程序,重点分析程序中3sleep函数的作用,如果取消掉用户处理函数中的两个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;
}

SIGUSR1SIGUSR2信号。用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

上述函数的正常执行结果本应该是:父进程数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;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/385321.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Leetcode | 03】String

字符串目录序号题号33. 无重复字符的最长子串 151. 翻转字符串里的单词

可/不可重入函数

一个函数在被调用执行期间&#xff08;尚未调用结束&#xff09;&#xff0c;由于某种时序&#xff08;递归或者处理信号捕捉时等情况&#xff09;又被重复调用&#xff0c;称之为“重入”。根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种。看如下程序。 可以看…

【Leetcode | 顺序刷题】杂项目录

序号题号类别1136. 只出现一次的数字位运算2137. 只出现一次的数字 II位运算3 260. 只出现一次的数字 III 位运算4191. 位1的个数位运算5231. 2的幂位运算6342. 4的幂位运算7 338. 比特位计数 位运算8405. 数字转换为十六进制数位运算9371. 两整数之和位运算10401. 二进制手表位…

SIGCHLD信号

&#xff08;1&#xff09;SIGCHLD信号产生的条件 1.子进程终止时会向父进程发送SIGCHLD信号&#xff0c;告知父进程回收自己&#xff0c;但该信号的默认处理动作为忽略&#xff0c;因此父进程仍然不会去回收子进程&#xff0c;需要捕捉处理实现子进程的回收&#xff1b; 2.子…

信号传参

&#xff08;1&#xff09;发送信号传参 前面已经知道从一个进程向另一个进程发送信号可以使用kill函数&#xff0c;但是kill函数在向进程发送信号的时候不能携带除了信号以外的其他信息&#xff0c;这时可以使用与kill相对应的sigqueue函数&#xff0c;该函数也是向一个进程发…

【Leetcode | 52】257. 二叉树的所有路径

给定一个二叉树&#xff0c;返回所有从根节点到叶子节点的路径。 说明: 叶子节点是指没有子节点的节点。 示例: 输入: 1 / \ 2 3 \ 5 输出: ["1->2->5", "1->3"] 解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3 解法一&a…

623. 在二叉树中增加一行

给定一个二叉树&#xff0c;根节点为第1层&#xff0c;深度为 1。在其第 d 层追加一行值为 v 的节点。 添加规则&#xff1a;给定一个深度值 d &#xff08;正整数&#xff09;&#xff0c;针对深度为 d-1 层的每一非空节点 N&#xff0c;为 N 创建两个值为 v 的左子树和右子树…

终端的概念

操作系统接口&#xff1a;用户接口和程序接口。用户接口分为联机用户接口和脱机用户接口。脱机用户接口出现在早期的批处理系统中&#xff08;将作业提前交给操作系统&#xff0c;作业完成的过程中用户无法交互&#xff09;&#xff1b;联机用户接口即为终端&#xff08;所有输…

终端的启动流程

在Linux操作系统启动时&#xff0c;首先加载的进程就是init进程&#xff08;ID为1&#xff09;&#xff0c;其余进程都是init进程产生的&#xff08;fork&#xff0c;然后exec金蝉脱壳&#xff09;&#xff0c;因此系统中所有进程都可以看成是init进程的子孙进程。可以通过ps a…

进程组(作业)

&#xff08;1&#xff09;概念和特性 进程组&#xff0c;也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念&#xff0c;是为了简化对多…

437. 路径总和 III

给定一个二叉树&#xff0c;它的每个结点都存放着一个整数值。 找出路径和等于给定数值的路径总数。 路径不需要从根节点开始&#xff0c;也不需要在叶子节点结束&#xff0c;但是路径方向必须是向下的&#xff08;只能从父节点到子节点&#xff09;。 二叉树不超过1000个节…

会话(session)

一组进程形成一个进程组&#xff0c;一组进程组形成一个会话&#xff0c;即一个会话中可以包括多个进程组。 &#xff08;1&#xff09;创建会话 创建一个会话需要注意以下6点注意事项&#xff1a;1.调用进程不能是进程组组长&#xff08;不能是父进程&#xff09;&#xff0…

508. 出现次数最多的子树元素和

给出二叉树的根&#xff0c;找出出现次数最多的子树元素和。一个结点的子树元素和定义为以该结点为根的二叉树上所有结点的元素之和&#xff08;包括结点本身&#xff09;。然后求出出现次数最多的子树元素和。如果有多个元素出现的次数相同&#xff0c;返回所有出现次数最多的…

1003 我要通过!(20)(20 分)

“答案正确”是自动判题系统给出的最令人欢喜的回复。本题属于PAT的“答案正确”大派送 —— 只要读入的字符串满足下列条件&#xff0c;系统就输出“答案正确”&#xff0c;否则输出“答案错误”。 得到“答案正确”的条件是&#xff1a; 1. 字符串中必须仅有P, A, T这三种字符…

网络终端

虚拟终端或串口终端的数目是有限的&#xff0c;虚拟终端&#xff08;字符控制终端&#xff09;一般就是/dev/tty1∼/dev/tty6六个&#xff0c;串口终端的数目也不超过串口的数目。然而网络终端或图形终端窗口的数目却是不受限制的&#xff0c;这是通过伪终端&#xff08;Pseudo…

线程的概念

线程&#xff08;LWP&#xff0c;light weight process&#xff09;是轻量级的进程&#xff0c;本质仍是进程&#xff08;在类unix环境下&#xff09;。进程有独立地址空间&#xff0c;拥有PCB&#xff1b;线程也有PCB&#xff0c;但没有独立的地址空间&#xff08;共享&#x…

1001. 害死人不偿命的(3n+1)猜想 (15)

卡拉兹(Callatz)猜想&#xff1a; 对任何一个自然数n&#xff0c;如果它是偶数&#xff0c;那么把它砍掉一半&#xff1b;如果它是奇数&#xff0c;那么把(3n1)砍掉一半。这样一直反复砍下去&#xff0c;最后一定在某一步得到n1。卡拉兹在1950年的世界数学家大会上公布了这个猜…

海量数据处理 (一)

现有海量日志数据保存在一个超级大的文件中&#xff0c;该文件无法直接读入内存&#xff0c;要求从中提取某天出访问百度次数最多的那个IP。 从这一天的日志数据中把访问百度的IP取出来&#xff0c;逐个写入到一个大文件中;注意到IP是32位的&#xff0c;最多有2^32个IP。同样可…

线程控制原语之pthread_self和pthread_create函数

注意&#xff1a;使用线程库函数用gcc编译时&#xff0c;要加参数&#xff1a;-lpthread&#xff08;libpthread.so&#xff09;&#xff0c;因为线程库函数属于第三方c库函数&#xff0c;不是标准库函数&#xff08;/lib、/usr/lib或者/usr/local/lib&#xff09;。 &#xf…

1005. 继续(3n+1)猜想 (25)

卡拉兹(Callatz)猜想已经在1001中给出了描述。在这个题目里&#xff0c;情况稍微有些复杂。 当我们验证卡拉兹猜想的时候&#xff0c;为了避免重复计算&#xff0c;可以记录下递推过程中遇到的每一个数。例如对n3进行验证的时候&#xff0c;我们需要计算3、5、8、4、2、1&#…