时序竞态

CPU进程之间的竞争

pause函数

调用该函数的进程主动挂起,等待任意信号唤醒,然后执行这个信号的默认行为,想要让这个程序继续运行就需要对这个信号进行捕捉,执行自定义的响应函数

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
void catch(int signo){
;
}

unsigned int mysleep(unsigned int seconds){
int ret;
struct sigaction act, oldact;

act.sa_flags= 0;
sigemptyset(&act.sa_mask);
act.sa_handler = catch;

ret = sigaction(SIGALRM, &act, &oldact);
if(ret == -1){
perror("sigaction error");
exit(1);
}

alarm(seconds);
ret = pause();

if(ret == -1 && errno == EINTR){
printf("pause sucess\n");
}

ret = alarm(0);//手动结束闹钟//当出现预期之外的信号干扰而唤醒时,提前结束这个闹钟
sigaction(SIGALRM, &oldact, NULL);//还原信号捕捉函数的系统默认设置(上一个旧设置)
return ret;
}
int main(){
while (1)
{
mysleep(3);
printf("-------------------\n");
}

return 0;
}

然而这里就会出现了一个比较致命的问题,设想这么个情况

假设在mysleep的闹钟函数中,

确实执行了定时器alarm函数成功定时了(这里假设只定时了1秒),准备执行pause时突然有其他进程抢占了CPU导致这个pause暂时得不到资源运行,足足停止了2秒钟没有运行,这时定时器已经发送过信号了,可这个信号是被捕捉的信号什么也不做的前提下,pause却这个时候执行了,进程挂起,可是再也收不到信号了,就会一直等啊等,导致整个进程阻塞。

在没有其他的函数处理这个时序竞态问题的话,首先想到的方法:

  • 在响应函数中,进行一个pause语句是否执行的判断(利用一个全局变量接受pause的返回值就行),如果没有执行,就再一次重置该闹钟。直到pause语句能够执行。

这个方法我感觉可行,没有测试过,有能力条件的可以测试一下运行几千几万次试试效果。

第二个方法:

  • 函数sigsuspend()与信号集操作函数代替pause()的阻塞功能

sigsuspend

函数的作用就是设定一个临时的信号屏蔽字,当收到其中一个没有被信号之前,进行一个挂起的操作pause,从而代替pause

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
void catch(int signo){
;
}

unsigned int mysleep(unsigned int seconds){
int ret;
struct sigaction act, oldact;
sigset_t newmask, oldmask, susmask;

act.sa_flags= 0;
sigemptyset(&act.sa_mask);
act.sa_handler = catch;
ret = sigaction(SIGALRM, &act, &oldact);
if(ret == -1){
perror("sigaction error");
exit(1);
}
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);//将之前进程中的信号屏蔽字取出放入oldmask

susmask = oldmask;
sigdelset(&susmask, SIGALRM);//保证这个信号在临时的信号集没有被屏蔽

alarm(seconds);
ret = alarm(0);//手动结束闹钟//当出现预期之外的信号干扰而唤醒时,提前结束这个闹钟
sigaction(SIGALRM, &oldact, NULL);//还原信号捕捉函数的系统默认设置(上一个旧设置)
return ret;

sigsuspend(&susmask);
int unslept = alarm(0);//手动结束闹钟//当出现预期之外的信号干扰而唤醒时,提前结束这个闹钟
sigaction(SIGALRM, &oldact, NULL);//恢复对信号SIGALRM的处理动作
sigprocmask(SIG_SETMASK,&oldmask,NULL);//恢复对信号进行屏蔽前的设置
return unslept;
}
int main(){
while (1)
{
mysleep(3);
printf("-------------------\n");
}

return 0;
}

时序竞态的产生原因

CPU的竞争,系统负载

也表明了信号确实也不是很可靠啊

解决方法:

主动预见其产生,主动编写函数处理,原子操作

全局变量的异步IO

分析父子进程交替数数程序,当前捕捉函数里的sleep取消,就会出现的必然问题(时序竞态的问题放大)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>

int count = 0;
int flag = 0;
void ParentCatch(int signo){
printf("I am Parent %d\t%d\n", getpid(), count);
count += 2;
flag = 1;
//sleep(1);
}
void ChildCatch(int signo){
printf("I am Child %d\t%d\n", getpid(), count);
count += 2;
flag = 1;
// sleep(1);
}

int main(){
pid_t pid, wpid;
struct sigaction act;
if((pid = fork()) < 0){
perror("fork error");
exit(1);
}else if(pid == 0){
count = 2;
act.sa_handler = ChildCatch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);
while(1){
if(flag == 1){
kill(getppid(),SIGUSR2);
flag = 0;
}
}
}else{
count = 1;
sleep(1); //保证子进程注册信号捕捉
act.sa_handler = ParentCatch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR2, &act, NULL);

ParentCatch(0);

while(1){
if(flag == 1){
kill(pid,SIGUSR1);
flag = 0;
}
}
}
return 0;
}

正常有sleep的时运行是没有问题的,可去掉sleep后运行之后可以发现,明明应该无限执行的进程,突然就死在某个数字上不动了,说明,发生了异常,这个异常的本质原因就是时序竞态,而直接原因,就是sleep被删除,没有足够时间进行CPU资源的争抢导致信号没发出去,比如子/父进程不检查flag值,也没有发送信号。

解决方法:

  • 去掉flag这个用来判断的全局变量,很显然的,flag根本没用,多此一举,直接在信号响应函数中发送信号,就没这么多屁事了
  • 还可以使用锁,锁住这个flag全局变量。但是没必要()

可重入与不可重入函数

可重入函数

一个函数在调用执行期间由于某种时序又被调用,称之为“重入”

递归函数就是一个重入函数形式

重入函数不应该含有全局变量,不要有malloc和free!

信号处理函数中有些可以重入,有些不可重入

safely called inside(可重入)

不可重入的:

  • 包含静态与全局
  • 标准的I/O函数
  • 有内存操作 malloc free new delete

子进程信号SIGCHLD

信号的产生条件

  • 子进程终止时
  • 子进程接收到SIGSTOP时
  • 子进程处在停止态,接收到SIGCONT后唤醒时

利用这个信号我们也可以回收子进程,不过需要注意信号集是个set集合

信号传参

信号也可以携带一定量的参数数据,

可以使用sigqueue函数对应kill函数

可在指定进程发送信号的同时携带参数,可以是一个int 和一个指针

1
int sigqueue(pid_t pid, int sig, const union sigval value);

捕捉信号传参

捕捉信号发送的参数,同样也可以使用sigaction函数 在传出参数的int&

struct sigaction 的 void (sa_sigaction)(int, siginfo_t , void *);

捕捉到相关信号的参数存储在 int 或者 siginfo_t* 的类型的结构体中

具体结构体可查阅man文档,需要注意的使用的时候需要配置sa_flag

中断系统调用

慢速的系统调用

会永久阻塞系统调用进程的

  • pause
  • wait
  • read
    • 读空洞,读网络,读管道的时候,读设备(键盘等)
  • 等等

其他系统调用:

  • 除了慢速系统调用都是

慢速的系统调用 收到信号打断,或者唤醒

当慢速系统调用收到信号打断,就被成为终端系统调用

在慢速系统调用中我们需要对信号捕捉判断处理除了判断是否成功捕获还需要判断errno 是否为EINTR(表示慢速系统调用函数被信号打断)

打断后的处理方式通过sa_flag的配置

进程组

主进程默认创建了无数个子进程

子进程的进程组ID实际上就是主进程的PID

进程组操作函数

getpgrp();
获取当前进程的进程组id

getpgid(pid);
获取指定进程的进程组ID

setpgid(pid,pgid);
设置指定进程的父进程id

结合使用代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

int main(){
pid_t pid;
pid = fork();
if(pid < 0){
perror("fork error");
exit(1);
}else if(pid > 0){
sleep(1);
setpgid(pid, pid);//让子进程脱离父进程

sleep(7);
printf("我是父进程:id:%u \n", getpid());
printf("我是父进程的父进程的id:id:%u \n", getppid());
printf("我是父进程的所属的进程组:id:%u \n", getpgrp());
printf("设置父进程的的进程组是自己父进程\n");
setpgid(getpid(), getppid());
printf("我是父进程的所属的进程组:id:%u \n", getpgrp());
}else{
printf("子进程的pid %u 所属进程组 %u\n", getpid(), getpgrp());
sleep(5);
printf("子进程的pid %u 所属进程组 %u\n", getpid(), getpgrp());
exit(0);
}
return 0;
}

会话(session)

进程组是进程的集合

而进程组的集合,就是会话

创建会话

  • 创建会话的进程不能是已有的组长进程
  • 创建会话的进程会成为一个新进程组的组长进程
  • 需要root权限(ubuntu不需要)
  • 新会话丢弃原有的控制终端,该会话没有控制终端
  • 调用进程是组长进程出错返回
  • 建立新会话时 ,执行顺序为:fork 终止父进程,子进程调用setsid。

子进程是野鸡变凤凰了,自立门户并且成为组长,会话ID也变成自己

守护进程

Daemon(精灵)进程,是linux的后台服务进程,通常独立于控制终端并且周期性执行某种任务或等待处理。

可以理解为后台服务,没有控制终端,很多服务器进程就是守护进程

一般守护进程名字末尾都是d

创建一个守护进程

  • 最关键的步骤其实就是创建会话
    • 创建子进程,关闭主进程,创建会话
  • 改变当前工作目录为根目录chdir();
    • 防止占用可卸载的文件系统(U盘,扩展内存卡等外设存储设备)
    • 可以更改其他路径
  • 重设文件权限掩码umask一般都是0002,有特殊需要就设置
  • 关闭文件描述符标准输出输入和出错 fd[0,1,2]
    • 不过一般都不关闭,而是进行重定向到黑洞NULL文件
    • dup2(fd[1], null)
  • 开始执行守护进程的核心进程

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int mydaemon(){

//第一步,子进程创建
pid_t pid;
pid = fork();
if(pid > 0){
return 0;
}else if(pid < 0){
perror("fork");
exit(1);
}

pid_t sid = setsid();//子进程变更为会话

int ret = chdir("/home/icrad");//改变会话的工作目录
if(ret < 0){
perror("chdir");
exit(0);
}
mode_t mask = 0000; //改变会话的文件权限掩码
umask(mask);

close(STDIN_FILENO); //关闭一个输入端
int fd = open("/dev/null", O_RDWR); //打开黑洞文件
dup2(STDIN_FILENO, STDERR_FILENO); //把标准输出和错误全部都导向黑洞
dup2(STDIN_FILENO, STDOUT_FILENO);

}

int main(){

mydaemon();//创建一个守护进程
while (1)
{
printf("主进程运行中");
}
return 0;
}

守护进程特性

  • 不受用户注销,挂起的影响,除非你关机
  • 快乐

编写一个守护进程:每隔一定时间输出系统时间到一个文件中

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/time.h>
#include<signal.h>
#include<string.h>
#include<time.h>

void writeDate(int signo){

//打开一个文件
int fd_r = open("Date.log", O_CREAT|O_RDWR|O_APPEND, 0777);
if(fd_r < 0){
perror("open");
exit(1);
}
//方案一:出错,无法写入
//dup2(STDOUT_FILENO,fd_r);
//execlp("date", "date", NULL);

//方案2
time_t timp;
time(&timp);
char* time = asctime(localtime(&timp));
write(fd_r,time,strlen(time));
//close(fd_r);

}


int mydaemon(int sec){

//第一步,子进程创建
pid_t pid;
pid = fork();
if(pid > 0){
return 0;
}else if(pid < 0){
perror("fork");
exit(1);
}
pid_t sid = setsid();//子进程变更为会话
int ret = chdir("/home/icrad");//改变会话的工作目录
if(ret < 0){
perror("chdir");
exit(0);
}
mode_t mask = 0000; //改变会话的文件权限掩码
umask(mask);
close(STDIN_FILENO); //关闭一个输入端
int fd = open("/dev/null", O_RDWR); //打开黑洞文件
dup2(STDIN_FILENO, STDERR_FILENO); //把标准输出和错误全部都导向黑洞
dup2(STDIN_FILENO, STDOUT_FILENO);

struct itimerval settime;
settime.it_interval.tv_sec = sec;
settime.it_interval.tv_usec = 0;
settime.it_value.tv_sec = sec;
settime.it_value.tv_usec = 0;

signal(SIGALRM, writeDate);
setitimer(ITIMER_REAL, &settime, NULL);
}

int main(int argc, char* argv[]){
if(argc != 2){
printf("请输入打印间隔时间\n");
return 0;
}
int time = atoi(argv[1]);
if(time > 0){
printf("打印间隔为%ds\n", time);
}else{
printf("打印间隔输入有误,默认为5秒\n");
time = 5;
}
mydaemon(time);//创建一个守护进程
for( ;; );
return 0;

}