竞态条件(时序竞态)
个人理解:由于系统中各个进程抢占cpu时间,导致本应该按照时间发生的事情没有发生,造成时间上的混乱。
pause函数
-
函数原型 int pause(void);
-
作用:挂起当前进程,等待信号来唤醒,处理的信号必须捕捉,否则信号默认动作导致进程直接停止。
-
返回值:
- 如果信号的处理动作为默认动作,则进程终止,pause函数没机会返回
- 如果信号的处理动作为忽略操作,则进程处于挂起状态,函数没有机会返回
- 如果信号被捕捉,那么首先处理捕捉函数,然后返回-1
- pause函数收到的信号不能被屏蔽,否则进程一直挂起,无法被唤醒
-
使用pause和alarm实现的sleep函数:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
void fun(int signo) {
cout << "SIGALRM has catched" << endl;
}
void mysleep(int sec) {
int ret = alarm(sec);
if(ret < 0) {
perror("alarm failed");
exit(1);
}
// 标记,下文会用到
ret = pause();
if(ret == -1 && errno == EINTR) {
puts("pause success");
}
}
int main(void) {
struct sigaction sig;
sig.sa_handler = fun;
sigemptyset(&sig.sa_mask);
sig.sa_flags = 0;
int ret = sigaction(SIGALRM, &sig, NULL);
if(ret == -1) {
perror("sigaction failed");
exit(1);
}
mysleep(3);
return 0;
}
时序竞态
前导例
欲睡觉,定闹钟十分钟,十分钟后闹钟叫醒自己
正常:定时,睡觉,十分钟后被叫醒
异常:定时,被叫走劳动二十分钟,然后回来,闹钟已经响过,不能把我唤醒
时序问题分析
我们分析alarm实现的sleep,如果在“标记”处cpu失去时间,当前进程进入等待状态,sec秒过后,然后信号已经发送完毕,但是pause还没有执行,导致进程永久挂起,虽然这种情况比较概率比较低,但是发生一次将会是致命的错误,尤其是在服务器当中
针对解决mysleep的的时序问题
我们考虑,如果在执行pause函数之前我们屏蔽了SIGALRM信号,然后在执行pause函数的同时解除SIGALRM信号的屏蔽,是不是可以让alarm发挥作用,sigsuspend可以做到这一点。
sigsuspend函数
- 函数原型:int sigsuspend(const sigset_t * mask);
- 函数作用:主动挂起进程,并且使用mask作为阻塞信号集(信号屏蔽字)
- 返回值:成功返回-1,并设置errno为EINTR
我们通过sigsuspend实现sleep来避免时序竞态带来的后果,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <iostream>
using std::cout;
using std::endl;
void callback(int signo) {
puts("signal has catched");
}
void mysleep(unsigned int sec) {
sigset_t newset, oldset, susmask;
int ret = 0;
sigemptyset(&newset);
sigaddset(&newset, SIGALRM);
ret = sigprocmask(SIG_BLOCK, &newset, &oldset);
if(ret == -1) {
perror("sigprocmask failed");
exit(1);
}
struct sigaction action, oldaction;
action.sa_handler = callback;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
ret = sigaction(SIGALRM, &action, &oldaction);
if(ret == -1) {
perror("sigaction failed");
exit(1);
}
ret = alarm(sec);
susmask = oldset;
if(sigismember(&susmask, SIGALRM)) {
sigdelset(&susmask, SIGALRM);
}
sigsuspend(&susmask);
ret = sigaction(SIGALRM, &oldaction, &action);
if(ret == -1) {
perror("sigaction failed2");
exit(1);
}
ret = alarm(0);
sigprocmask(SIG_SETMASK, &oldset, &newset);
}
int main() {
mysleep(1);
cout << "***" << endl;
return 0;
}
全局变量异步I/O
我们使用父进程和子进程轮流数数代码:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int n;
pid_t pid;
void parent_fun(int signo) {
printf("I'm parent, my pid = %d, n = %d\n", getpid(), n);
n += 2;
kill(pid, SIGUSR1);
//sleep(1);
}
void son_fun(int signo) {
printf("I'm son, my pid = %d, n = %d\n", getpid(), n);
n += 2;
kill(getppid(), SIGUSR2);
//sleep(1);
}
int main(void) {
if((pid = fork()) == -1) {
perror("fork error");
exit(1);
} else if(pid > 0) {
n = 1;
struct sigaction action;
action.sa_handler = parent_fun;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGUSR2, &action, NULL);
sleep(1);
parent_fun(0);
while(1) {
}
} else {
n = 2;
struct sigaction action;
action.sa_handler = son_fun;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGUSR1, &action, NULL);
while(1) {
}
}
return 0;
}
可重入/不可重入函数
可重入函数和不可重入的区别,可重入函数不存在全局和static数据结构,malloc/free,也不能使用标准IO。
捕捉SIGCHLD信号
什么时候会产生SIGCHLD信号:
1. 子进程终止死亡。
2. 子进程接收到SIGSTOP信号,进入暂停状态
3. 子进程接收到SIGCONT信号,进入继续执行状态
使用SIGCHLD回收子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
void do_wait(int signo) {
pid_t pid = 0;
int status;
//注意下面是while
while((pid = waitpid(0, &status, WNOHANG)) > 0) {
if(WIFEXITED(status)) {
printf("child %d exit with %d\n", pid, WEXITSTATUS(status));
} else if(WIFSIGNALED(status)) {
printf("child %d signal by %d\n", pid, WTERMSIG(status));
}
}
}
int main(int argc, char ** argv) {
pid_t pid;
int i = 0;
printf("%d %s\n", argc, argv[0]);
sleep(1);
for(i = 0; i < 10; i++) {
if((pid = fork()) == 0) {
break;
} else if(pid == -1) {
perror("fork failed");
exit(1);
}
}
if(pid == 0) {
printf("child id = %d\n", getpid());
sleep(1);
return i + 1;
} else if(pid > 0) {
struct sigaction action;
action.sa_handler = do_wait;
action.sa_flags = 0;
sigemptyset(&action.sa_mask);
sigaction(SIGCHLD, &action, NULL);
while(1) {
printf("parent id = %d\n", getpid());
sleep(1);
}
}
return 0;
}
中断系统调用
慢速系统调用
介绍:可能会永久阻塞进程的一类,在接收到信号时该函数会中断,不在执行,也可以设置为重新启动,如read,write,pause,wait,waitpid等
其他系统调用
除慢速系统调用的系统调用
守护进程
什么是守护进程
守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。通俗来说就是存在一些进程,一直在运行,不停的监听各种操作,守护进程的的名字后边一般有个d,如ftpd,httpd等。
如何创建守护进程
- 创建父进程,并通过父进程fork子进程,父进程退出。
- 使用setsid()函数设置子进程为新的会话会长和组长,脱离与父进程的关系
- 改变工作目录为根目录
- 重新设置umask掩码
- 关闭没有必要的文件描述符(把0, 1, 2重定向到/dev/null)
- 守护进程主逻辑
- 守护进程退出