进程信号

目录

进程信号

一、信号的理解

1.1.信号的概念

1.2.信号的结论

1.3.信号的种类

1.4.信号的处理

1.5.signal函数

1.7.目标进程

1.8.jobs命令

1.9.fg命令

1.10.ctrl + z命令

1.11.bg命令

1.12.信号的记录

1.13.信号与通信

 二、信号的产生

2.1.键盘产生的信号

2.2.系统调用产生的信号

2.2.1.kill函数

2.2.2.raise函数

2.2.3.abort函数

2.3.系统命令产生的信号

2.3.1.kill命令

2.4.硬件异常产生的信号

2.4.1.除0错的信号

2.4.2.野指针错的信号

2.4.3.core dump核心转储

2.4.4.gbd调试core文件

2.5.软件条件产生的信号

2.5.1.管道通信的软件异常

2.5.1.alarm函数

2.5.2.pause函数

2.5.3.系统闹钟的理解

三、信号的保存

3.1.信号的其他概念

3.2.信号的内核示意图

3.3.sigset_t类型

3.4.信号集操作函数

3.4.1.sigprocmask函数

3.4.2.sigpending函数

四、信号捕捉

4.1.信号捕捉的流程

4.2.用户态和内核态

4.2.1.硬件中断

4.2.2.时钟中断

4.3.3.死循环

4.3.4.软中断(异常)

4.3.5.软中断(陷阱)

4.3.6.理解用户态与内核态

4.3.signaction函数

五、可重入函数

六、volatile

七、SIGCHLD信号


一、信号的理解

1.1.信号的概念

信号:事件的异步通知机制

异步通知

  • 同步:我们自习一会,等张三同学回来再讲课
  • 异步:我们继续上课,张三去取快递

生活中的信号:闹钟,上课铃声

  • 闹钟:睡觉 -> 醒来
  • 上课铃声:休息 -> 听课

信号的特点:会中断当前正在做的事情

信号:一种给进程发送的,用来进行事件异步通知的机制

信号发送给进程,信号的产生相对于进程的运行是异步的

1.2.信号的结论

进程在信号没有产生的时候,早就知道该如何处理信号

信号的处理不是立即处理,而是在合适的时候进行处理

进程是提前被设计过的,内置了信号的识别与处理方式

给进程产生信号的信号源非常多

1.3.信号的种类

Linux有62种信号

  • 34-64为实时信号:需要立即处理
  • 1-31为普通信号:可以不被立即处理

man 7 signal

1.4.信号的处理

默认处理、自定义处理、忽略处理

1.5.signal函数

作用:自定义进程对信号的捕捉方式

参数1:信号编号

参数2:自定义捕捉方式

实验:自定义捕捉SIGINT信号

#include <iostream>
#include <unistd.h>
#include <signal.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
	signal(SIGINT,handlerSig);

	int cnt = 0;
	while(true)
	{
		std::cout << "hello,world, " << cnt++ << std::endl;
		sleep(1);
	}

	return 0;
}

实验现象

1.7.目标进程

前台进程(#./xxx.exe):可以向标准输出打印内容,可以从标准输入获取内容

后台进程(#./xxx.exe &):可以向标准输出打印内容,无法从标准输入获取内容

前台进程本质:从键盘获取数据的进程

前台进程必须只有一个,后台进程可以有多个

命令行shell进程:默认为前台进程,执行其他前台程序时,自动变为后台进程

父进程先退出,子进程变为孤儿进程,是后台进程,ctrl + c无法杀掉

1.8.jobs命令

作用:查看当前所有的后台任务

1.9.fg命令

fg 任务号:将指定后台进程提到前台

1.10.ctrl + z命令

ctrl + z:暂停进程,将前台进程自动提到后台

1.11.bg命令

bg 任务号:将指定后台进程恢复运行

1.12.信号的记录

信号产生后并不是立即处理的,进程必须记录信号在合适的时候进行处理

信号会被记录在进程的PCB的一个无符号整型变量中,看作一个位图结构

  • 比特位的位置代表信号的编号
  • 比特位的内容代表信号是否收到

向目标进程发送信号的本质:获取目标进程pid与信号编号,修改位图

PCB属于OS内的数据结构对象,修改位图的本质是修改内核的数据

所以不管信号怎么产生,发送信号在底层必须让给操作系统来发送

操作系统必须给用户提供发送信号的系统调用函数

1.13.信号与通信

信号:用户与进程之间交互

通信IPC:进程与进程之间交互

 二、信号的产生

2.1.键盘产生的信号

ctrl + c:给目标进程发送2号信号,进程处理该信号的动作是让自己终止

ctrl + \:给目标进程发送3号信号,进程处理该信号的动作是让自己终止并生成core dump文件

ctrl + z:给目标进程发送20号信号,进程处理该信号的动作是让自己暂停,并挂到后台

2.2.系统调用产生的信号

大部分信号可以被自定义捕捉,但是9号信号无法被自定义捕捉

2.2.1.kill函数

作用:指定一个进程,发送指定的信号

参数1:目标进程pid

参数2:信号编号

 

实验:使用kill函数向指定进程发送指定信号

  • mykill.cc
#include <iostream>
#include <sys/types.h>
#include <signal.h>

//./mykill signumber pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cout << "./mykill signumber pid" << std::endl;
        return 1;   
    }

    //获取信号编号
    int signum = std::stoi(argv[1]);
    //获取目标进程pid
    pid_t target = std::stoi(argv[2]);

    int n = kill(target, signum);
    if(n == 0)
    {
        std::cout << "send " << signum << " to " << target << " success" << std::endl;
    }

    return 0;
}
  • testSig.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
	signal(SIGINT,handlerSig);
	int cnt = 0;
	while(true)
	{
		std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
		sleep(1);
	}
	return 0;
}
  • Makefile
.PHONY:all
all:testsig mykill

testsig:testSig.cc
	g++ -o $@ $^ -std=c++11
mykill:mykill.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f testsig mykill

实验现象

2.2.2.raise函数

作用:进程给自己发送任意信号

参数:信号编号

实验:给自己进程发送信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
	for(int i = 1; i < 32; i++)
	{
		signal(i, handlerSig);
	}
	for(int i = 1;i < 32; i++)
	{
		sleep(1);
		raise(i);
	}
	int cnt = 0;
	while(true)
	{
		std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
		sleep(1);
	}
	return 0;
}

实验现象

2.2.3.abort函数

作用:终止进程

实验:使用abort函数终止进程

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
}

int main()
{
	for(int i = 1; i < 32; i++)
	{
		signal(i, handlerSig);
	}

	int cnt = 0;
	while(true)
	{
		std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
		abort();
		sleep(1);
	}
	return 0;
}

实验现象

2.3.系统命令产生的信号

2.3.1.kill命令

kill命令

2.4.硬件异常产生的信号

2.4.1.除0错的信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
	exit(13);
}

int main()
{
	for(int i = 1; i < 32; i++)
	{
		signal(i, handlerSig);
	}

	int cnt = 0;
	while(true)
	{
		std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
		int a = 10;
		a /= 0;//除0错误
		sleep(1);
	}
	return 0;
}

实验现象

SIGFPE(信号编号8):浮点数溢出错误

2.4.2.野指针错的信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
	exit(13);
}

int main()
{
	for(int i = 1; i < 32; i++)
	{
		signal(i, handlerSig);
	}

	int cnt = 0;
	while(true)
	{
		std::cout << "hello,world, " << cnt++ << " ,pid: " << getpid() << std::endl;
		sleep(1);
		int *p = nullptr;//野指针错
		*p = 100;
	}
	return 0;
}

实验现象

SIGSEGV(信号编号11):段错误

操作系统如何知道进程出错

浮点数溢出报错:

CPU的标志寄存器EFLAGS中有一个比特位表示CPU当前运算时是否出现溢出

CPU属于硬件,操作系统是软硬件资源的管理者,可以识别CPU中寄存器情况

CPU寄存器保存的是当前进程的上下文与PCB,发生溢出后操作系统发送信号

野指针报错:

CPU的CR3寄存器保存着当前进程对应页表的起始地址

CPU内部需要寻址的地址是虚拟地址,CPU将虚拟地址和CR3的页表地址交给MMU硬件单元

进行虚拟地址与物理地址转换,如果MMU转换失败,就会发送硬件报错,操作系统发送信号

2.4.3.core dump核心转储

Core Dump:核心转储

进程异常退出前,进程在内存中的核心数据,从内存拷贝到磁盘,在当前路径形成一个文件

在云服务器上,该功能是被禁止的,为了防止服务器出错时,多次打开关闭,出现大量的core文件

#include <iostream>

int main()
{
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	int a = 10;
	a /= 0;
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	return 0;
}

 

信号为0[1 ~31]:进程正常终止,没有接收到信号

core dump标志:是否发送核心转储

#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		printf("hello\n");
		printf("hello\n");
		printf("hello\n");
		printf("hello\n");
		int a = 10;
		a /= 0;
		printf("hello\n");

		exit(1);
	}
	int status = 0;
	waitpid(id, &status, 0);
	printf("signal: %d, exit code: %d, core dump: %d\n",
		  (status & 0x7f), (status >> 8) & 0xff, (status >> 7) & 0x01);

	return 0;
}

如果不设置core dump,则不会发生核心转储

Term:进程直接退出

#include <iostream>
#include <signal.h>

int main()
{
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	raise(2);
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	return 0;
}

2.4.4.gbd调试core文件

开启core dump,直接运行崩溃,使用gdb的core-file命令,直接定位到出错行

#include <iostream>

int main()
{
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	int a = 10;
	a /= 0;
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	printf("hello\n");
	return 0;
}

//Makefile

testsig:testSig.cc
	g++ -o $@ $^ -g -std=c++11 
.PHONY:clean
clean:
	rm -f testsig

2.5.软件条件产生的信号

2.5.1.管道通信的软件异常

进程 === 管道 === 进程

读端关闭,软件条件不满足,写端再写时,会收到SIGPIPE信号

2.5.1.alarm函数

作用:为当前进程设置闹钟,闹钟到时间后,会给当前进程发送SIGALRM信号,终止进程

参数1:设置闹钟的时间长度,0-取消闹钟

返回值:

如果设定时间跑完:返回0

如果设定时间还没结束,又开启一个闹钟:返回上一个闹钟的剩余时间

实验:有IO操作,设置1s闹钟,1s后查看接收到的信号编号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig << std::endl;
	exit(13);
}

int main()
{
	for(int i = 1; i < 32; i++)
	{
		signal(i, handlerSig);
	}

	//设定1s闹钟,1s以后,当前进程会收到一个信号
	alarm(1);
	
	int cnt = 0;
	while(true)
	{
        //本质是IO xshell->./xxx->云服务器->网络->终端显示
		std::cout << "count: " << cnt++ << std::endl;
	}
}

实验现象

实验:无IO操作,设置1s闹钟,1s后查看接收到的信号编号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

int cnt = 0;

void handlerSig(int sig)
{
	std::cout << "获得了一个信号: " << sig  << " cnt: " << cnt << std::endl;
	exit(13);
}

int main()
{
	signal(SIGALRM, handlerSig);
	
	//设定1s闹钟,1s以后,当前进程会收到一个信号
	alarm(1);

	while(true)
	{
		cnt++;
	}
}

实验现象

有IO操作的运行效率比无IO操作的运行效率低很多

2.5.2.pause函数

作用:等待一个信号

实验:使用闹钟,每秒调度任务

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

typedef struct task_struct
{
	pid_t id;//进程pid
	int count = 10;//时间片 本质就是一个计数器
	void(*code)();
}t;

std::vector<task_struct> task_list;
int timestamp = 0;

//////////////func//////////////////
void Sched()
{
	std::cout << "我是进程调度" << std::endl;
    t->count--;
    if(t.count <= 0)
    {
        //切换其他进程
    }
}
void MemManger()
{
	std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;
}
void Fflush()
{
	std::cout << "我是刷新程序,我在定期刷新内存数据到磁盘" << std::endl;
}
//////////////func//////////////////

using func_t = std::function<void()>;

std::vector<func_t> funcs;

//每隔一秒 ,完成一些任务
void handlerSig(int sig)
{
    timestamp++;
	std::cout << "#########################################" << std::endl;
	for(auto f : funcs)
	{
		f();
	}
	std::cout << "#########################################" << std::endl;
	int n = alarm(1);
}

int main()
{
	funcs.push_back(Sched);
	funcs.push_back(MemManger);
	funcs.push_back(Fflush);
	
	signal(SIGALRM,handlerSig);
	alarm(1);

	while(true)//这就是操作系统
	{
		pause();
	}
	return 0;
}

实验现象

2.5.3.系统闹钟的理解

系统闹钟的本质:操作系统自身具有的定时功能,并能让用户设置这种定时功能

操作系统内同时存在很多闹钟,需要对多个闹钟管理(先描述,再组织)

struct timer_list 
{
    struct list_head entry;
    unsigned long expires;

    void (*function)(unsigned long);
    unsigned long data;

    struct tvec_t_base_s *base;
};

小根堆结构,堆顶是最短超时的闹钟,将堆顶的超时时间与系统时间戳进行比较

当堆顶节点出堆,堆结构重新调整为小根堆,让操作系统执行函数指针指向方法

向目标进程发送SIGALRM(定时器超时)

三、信号的保存

3.1.信号的其他概念

信号递达(delivery):处理信号的动作(自定义、默认、忽略)

信号未决(Pending):信号从产生到递达之间(信号在位图中,但未处理)

信号阻塞(屏蔽):进程可以阻塞信号,被阻塞的信号处于信号未决,解除阻塞后才能执行递达

老师在课堂布置作业,就是发信号的过程

同学将作业记录下来,就是进行记录信号

但同学未在课堂上写作业,此时作业处于信号未决

同学放学回家开始写作业,此时作业才被执行递达

同学不喜欢这个上课老师,但为了学分,只好正常上课,并且记录作业

但就是不写作业,作业被未决了,但不会被递达,该过程叫做信号阻塞

期末要考试时,同学将处于未决的作业全部写完,该过程叫做解除阻塞

注:信号被阻塞就不会递达,信号被递达后才能忽略

3.2.信号的内核示意图

进程PCB与普通信号有三张位图

pending(信号未决表):保存未决的信号的位图

  • 比特位的位置:信号编号
  • 比特位的内容:是否收到

block(信号阻塞表):保存阻塞的信号的位图

  • 比特位的位置:信号编号
  • 比特位的内容:是否阻塞

pending & ~blocks:能被递达的信号

handler:保存SIG_DFL(0默认信号)SIG_IGN(1忽略信号)sighandler(自定义捕获)

  • 数组下标:信号编号
  • 数组元素:信号处理函数

// 内核结构 2.6.18
struct task_struct 
{
    ...
    /* signal handlers */
    struct sighand_struct *sighand;
    sigset_t blocked
    struct sigpending pending;
    ...
}

struct sighand_struct 
{
    atomic_t count;
    struct k_sigaction action[_NSIG]; // #define _NSIG 64
    spinlock_t siglock;
};

struct __new_sigaction 
{
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    void (*sa_restorer)(void); /* Not used by Linux/SPARC */
    __new_sigset_t sa_mask;
};

struct k_sigaction 
{
    struct __new_sigaction sa;
    void __user *ka_restorer;
};

/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);

struct sigpending 
{
    struct list_head list;
    sigset_t signal;
};

Linux的常规信号在递达之前产生多次时,只会被记录一次

POSIX.1允许系统递送该信号一次或者多次

3.3.sigset_t类型

sigset_t:信号集类型

#ifndef ____sigset_t_defined
#define ____sigset_t_defined

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

阻塞信号集:有效和无效表示该信号是否阻塞

未决信号集:有效和无效表示该信号是否处于未决

阻塞信号集也叫做信号屏蔽字(Signal Mask),类似权限的位掩码umask

位图结构

index:39 / 32 = 1......bitmap[1]

pos:39 % 32  = 7......bitmap[1]的第七位

struct bis
{
	int bitmap[10];//32 * 10 = 320
}

3.4.信号集操作函数

#include <signal.h>

//全置为0
int sigemptyset(sigset_t *set);
//全置为1
int sigfillset(sigset_t *set);
//将比特位置1
int sigaddset(sigset_t *set, int signo);
//将比特位置0
int sigdelset(sigset_t *set, int signo);
//判断比特位是否为1
int sigismember(const sigset_t *set, int signo);
3.4.1.sigprocmask函数

作用:修改block表

参数1:如何操作

SIG_BLOCK:添加屏蔽信号,mask = mask | set

SIG_UNBLOCK:解除屏蔽信号,mask = mask & ~set

SIG_SETBLOCK:拷贝屏蔽信号,mask = set

参数2:设置修改值set

参数3:输出型参数,输出过去的set值

3.4.2.sigpending函数

作用:获取当前进程的pending信号集

参数:输出型参数,输出pending信号集

默认情况下,进程不对任何信号屏蔽

实验一:屏蔽2号信号,循环获取pending信号集并且打印,全为0

向进程发送2号信号,打印pending时2号信号所在比特位由0变1

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>

void PrintPending(sigset_t pending)
{
	printf("我是一个进程(%d), pending: ", getpid());
	for(int signo = 31; signo >= 1; signo--)
	{
		if(sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << std::endl;
}

int main()
{
	// 1. 创建阻塞位图
	sigset_t block, oblock;

	// 2. 清空阻塞位图
	sigemptyset(&block);
	sigemptyset(&oblock);

	// 3. 阻塞2号信号
	sigaddset(&block, SIGINT);						   // 设置阻塞位图
	int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
	(void)n;

	// 重复获取和打印
	while(true)
	{
		// 4. 创建未决位图
		sigset_t pending;

		// 5. 写入进程
		int m = sigpending(&pending);

		// 6. 打印未决位图
		PrintPending(pending);

		sleep(1);
	}

	return 0;
}

实验现象

实验二:验证9号信号不可被捕捉,不可被阻塞

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>

void PrintPending(sigset_t pending)
{
	printf("我是一个进程(%d), pending: ", getpid());
	for(int signo = 31; signo >= 1; signo--)
	{
		if(sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << std::endl;
}

int main()
{
	// 1. 创建阻塞位图
	sigset_t block, oblock;

	// 2. 清空阻塞位图
	sigemptyset(&block);
	sigemptyset(&oblock);

	// 3. 阻塞所有信号
	for(int i = 1; i < 32; i++)
	{
		sigaddset(&block, i);
	}
	int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
	(void)n;

	// 重复获取和打印
	while(true)
	{
		// 4. 创建未决位图
		sigset_t pending;

		// 5. 写入进程
		int m = sigpending(&pending);

		// 6. 打印未决位图
		PrintPending(pending);

		sleep(1);
	}

	return 0;
}

实验现象

实验三:验证2号信号递达后,pending由1变为0

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>

void PrintPending(sigset_t pending)
{
	printf("我是一个进程(%d), pending: ", getpid());
	for(int signo = 31; signo >= 1; signo--)
	{
		if(sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << std::endl;
}

void handler(int sig)
{
	std::cout << "递达 " << sig << " 信号!" << std::endl;
}

int main()
{
	signal(SIGINT, handler);
	// 1. 创建阻塞位图
	sigset_t block, oblock;

	// 2. 清空阻塞位图
	sigemptyset(&block);
	sigemptyset(&oblock);

	// 3. 阻塞2号信号
	sigaddset(&block, SIGINT);						   // 设置阻塞位图
	int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
	(void)n;

	// 重复获取和打印
	int cnt = 0;
	while(true)
	{
		// 4. 创建未决位图
		sigset_t pending;

		// 5. 写入进程
		int m = sigpending(&pending);

		// 6. 打印未决位图
		PrintPending(pending);
		if(cnt == 10)
		{
			std::cout << "解除对2号信号的屏蔽" << std::endl;
			sigprocmask(SIG_SETMASK, &oblock, nullptr);
		}
		sleep(1);
		cnt++;
	}

	return 0;
}

实验现象

实验四:验证是递达之前pending设置为0,还是递达之后pending设置为0

#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>

void PrintPending(sigset_t pending)
{
	printf("我是一个进程(%d), pending: ", getpid());
	for(int signo = 31; signo >= 1; signo--)
	{
		if(sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << std::endl;
}

void handler(int sig)
{
	std::cout << "递达 " << sig << " 信号!" << std::endl;
	sigset_t pending;
	int m = sigpending(&pending);
	PrintPending(pending);
	//0000 0010: 处理完,2号才被设置为0
	//0000 0000: 执行handler之前,2号pending已经被清理
	std::cout << "######################" << std::endl;
}

int main()
{
	signal(SIGINT, handler);
	// 1. 创建阻塞位图
	sigset_t block, oblock;

	// 2. 清空阻塞位图
	sigemptyset(&block);
	sigemptyset(&oblock);

	// 3. 阻塞2号信号
	sigaddset(&block, SIGINT);						   // 设置阻塞位图
	int n = sigprocmask(SIG_SETMASK, &block, &oblock); // 写入进程
	(void)n;

	// 重复获取和打印
	int cnt = 0;
	while(true)
	{
		// 4. 创建未决位图
		sigset_t pending;

		// 5. 写入进程
		int m = sigpending(&pending);

		// 6. 打印未决位图
		PrintPending(pending);
		if(cnt == 10)
		{
			std::cout << "解除对2号信号的屏蔽" << std::endl;
			sigprocmask(SIG_SETMASK, &oblock, nullptr);
		}
		sleep(1);
		cnt++;
	}

	return 0;
}

实验现象

在准备递达的时候,首先要清空pending信号集中对于的位图

四、信号捕捉

4.1.信号捕捉的流程

信号的处理是自定义捕获:

流程1:执行到主函数的某一条指令时,会因为中断、异常或者系统调用进入操作系统内核

流程2:内核处理完异常后,在准备回到用户模式前,会先处理当前进程中可以递达的信号

流程3:内核调用do signal函数,查询进程PCB中的三张信号表

流程4:如果此时信号对应的block为0,pending为1,handler为自定义捕获,则会跳转到用户层,

执行用户自定义的方法sighandler

流程5:在信号处理函数返回时,执行系统调用函数sigretum再次进入内核

流程6:在内核中返回用户模式,从上次被中断的地方继续往下执行程序

信号的处理是忽略处理:

操作系统将进程PCB中的pending对应比特位从1改为0,然后返回用户层

信号的处理是默认处理:

如果默认处理是终止,进程直接被杀,如果默认处理是暂停,就被挂载到,暂停相关的数据结构

必须以用户身份执行自定义方法,如果在内核中执行,出现非法操作时会出问题

进程凭什么进入内核

while(ture){}也会进入内核,因为进程会被调度到CPU,当时间片到了后

操作系统会把它从CPU强制剥离出来,进入内核态

4.2.用户态和内核态

4.2.1.硬件中断

操作系统怎么知道键盘上面有数据

外部设备在硬件上会与CPU的针脚进行间接的连接

步骤1:当外部设备就绪时,会向CPU的针脚发起硬件中断

步骤2:通过主板上的中断控制器接收中断信息通知到CPU

步骤3:CPU得知中断信息,获取中断号

注:在外部设备中也有寄存器

在磁盘设备中,有控制寄存器,地址寄存器,数据寄存器

当CPU给磁盘发送读写信息时,磁盘通过对应寄存器接收

操作系统会维护一张中断向量表,是操作系统的一部分,数组的下标是中断号

步骤4:CPU保护现场,保存当前进程上下文在寄存器中的值

步骤5:CPU根据中断号,执行中断处理函数

步骤6:执行完毕后恢复现场,继续之前工作

中断向量表

void trap_init(void)
{
	int i;

	set_trap_gate(0,&divide_error);
	set_trap_gate(1,&debug);
	set_trap_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_trap_gate(8,&double_fault);
	set_trap_gate(9,&coprocessor_segment_overrun);
	set_trap_gate(10,&invalid_TSS);
	set_trap_gate(11,&segment_not_present);
	set_trap_gate(12,&stack_segment);
	set_trap_gate(13,&general_protection);
	set_trap_gate(14,&page_fault);
	set_trap_gate(15,&reserved);
	set_trap_gate(16,&coprocessor_error);
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
	set_trap_gate(45,&irq13);
	outb_p(inb_p(0x21)&0xfb,0x21);
	outb(inb_p(0xA1)&0xdf,0xA1);
	set_trap_gate(39,&parallel_interrupt);
}

void rs_init(void)
{
	set_intr_gate(0x24,rs1_interrupt);
	set_intr_gate(0x23,rs2_interrupt);
	init(tty_table[1].read_q.data);
	init(tty_table[2].read_q.data);
	outb(inb_p(0x21)&0xE7,0x21);
}

信号的本质是用软件来模拟硬件中断

  • 发中断 -- 发信号
  • 保存中断号 -- 记录信号
  • 中断号 -- 信号编号
  • 处理中断 -- 信号处理
  • 外部设备 -- 信号源
  • 中断号 -- pending表
  • 中断向量表 -- handler表
  • 中断屏蔽 -- block表
4.2.2.时钟中断

集成在CPU内部的时钟源,以固定的频率向CPU发送中断

操作系统就可以在硬件时钟中断的驱动下,进行进程调度

计算机上电,从网络中获取当前时间,将时间转化时间戳

设置为历史总频率total,时钟源每触发一次中断total加一

就可以让计算机在离线的情况,也能够知道时间是几点了

CPU主频:中断触发的频率

操作系统:基于中断来进行工作的软件

4.3.3.死循环

 当中断未发生时,操作系统什么都不干,是暂停的

在硬件中断和时钟中断的推动下,自动地进行调度

4.3.4.软中断(异常)

异常:软中断的一种,软件导致硬件先出错,再由硬件触发的中断

常见异常:除0异常,野指针,指针重复释放,缺页异常

  • 除0异常:

在处理当前进程的代码1ns时间片内,如果进程在执行时出现除0错误

CPU内部的状态寄存器EFLAGS会发生溢出,将该错误规定成一种由CPU内部触发的中断

CPU会给自己生成一个中断号,操作系统进行中断服务,处理异常,给目标进程发送信号

  • 缺页异常:

要访问的物理内存空间在虚拟地址空间中已经申请,但是页表并没有进行映射

MMU会发生错误,产生缺页中断,操作系统进行中断服务,重新申请空间映射

4.3.5.软中断(陷阱)

陷阱:从用户态陷入内核态

为了让CPU通过软件主动中断,在CPU的指令集中引入了两个新的汇编指令

通过这两个指令,在CPU内部自动触发中断(C/C++代码本质就是编译成为了指令集+数据)

  • x86:int 0x80
  • x86_64:syscall

进行系统调用的时候,具体是怎么进入操作系统完成的

所有的系统调用都被写入一个系统调用函数指针表里面

每一个系统调用都有一个唯一的下标,叫做系统调用号

注:OS不会提供任何系统调用接口,只提供系统调用号,而open、fork这些函数都是glibc封装的

用户层(glibc)

  • x86

  • x86_64

#define SYS_ify(syscall_name) __NR_##syscall_name:将系统调用名称转为系统调用号

示例:SYS_ify(open):__NR_open

//linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.h 12
/* at least 8 syscall per cacheline */
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
#define __NR_stat 4
__SYSCALL(__NR_stat, sys_newstat)
#define __NR_fstat 5
__SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat 6
__SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll 7
__SYSCALL(__NR_poll, sys_poll)
#define __NR_lseek 8
__SYSCALL(__NR_lseek, sys_lseek)
#define __NR_mmap 9
__SYSCALL(__NR_mmap, sys_mmap)
#define __NR_mprotect 10
__SYSCALL(__NR_mprotect, sys_mprotect)
#define __NR_munmap 11
__SYSCALL(__NR_munmap, sys_munmap)
#define __NR_brk 12
__SYSCALL(__NR_brk, sys_brk)
#define __NR_rt_sigaction 13
__SYSCALL(__NR_rt_sigaction, sys_rt_sigaction)
#define __NR_rt_sigprocmask 14
__SYSCALL(__NR_rt_sigprocmask, sys_rt_sigprocmask)
#define __NR_rt_sigreturn 15
__SYSCALL(__NR_rt_sigreturn, stub_rt_sigreturn)
//...

内核层

4.3.6.理解用户态与内核态

系统调用的也是在进程地址空间上进行

所有的函数调用都是地址空间之间跳转

操作系统也是软件,一定也在内存当中

用户页表存在多份,内核页表只有一份

无论进程如何调度,总能够找到操作系统

用户和内核都在同一个[0,4]GB地址空间

如果用户随便用一个[3,4]GB的虚拟地址,就可以随便访问内核的代码与数据

但操作系统为了保护自己,不相信任何人,必须采用系统调用的方式进行访问

用户态:以用户身份,只能访问自己的[0,3]GB

内核态:以内核身份,通过系统调用访问[3,4]GB

如何区分当前处于内核态与用户态(段描述符表、RPL、DPL)

CPU中的CS寄存器(代码段寄存器)的低两位比特位

(Current Privilege Level,当前特权级)

  • 如果为0:内核态
  • 如果为3:用户态

如果处于用户态时,访问了内核中的代码与数据,就会触发硬件中断,终止进程

而int 0x80或者syscall指令,就可以让CS由用户态切换为内核态,执行系统调用

4.3.signaction函数

作用:检测并且改变信号处理动作

参数1:信号编号

参数2:信号处理结构体(设置自定义捕捉方法)

参数3:输出型参数,输出上次的信号处理动作

sa_handler:自定义捕捉方法

sa_mask:信号屏蔽字

正在处理某个信号时,为了防止某个信号再次被递达,操作系统会自动修改当前进程的block表

进行阻塞,所以在处理自定义捕捉方法之前,除了把pending表由1变0,还要把block表由0变1

如果在调用信号处理函数时,除了当前信号自动屏蔽,还希望屏蔽其他信号,需要用mask说明

实验:对2号信号进行捕捉,2、3、4都屏蔽

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>

void handler(int signum)
{
    std::cout << "hello signal: " << signum << std::endl;
    while(true)
    {
        //不断获取pending表
        sigset_t pending;
        sigpending(&pending);
        for(int i = 31; i >= 1; i--)
        {
            if(sigismember(&pending, i))
            {
                std::cout << "1";
            }
            else
            {
                std::cout << "0";
            }
        }
        std::cout << std::endl;
        sleep(1);
    }
    exit(0);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, &oact);
    while(true)
    {
        std::cout << "hello world: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

实验现象

五、可重入函数

main执行流与handler执行流是串行的,是在同一个进程里执行的

所以会导致insert方法被两个以上的执行流重复进入,即函数重入

如果在函数重入时,进程出现异常,称为不可重入函数

如果在函数重入时,进程没有异常,称为可重入函数

注:大多数函数,都是不可重入函数

比如:

调用了malloc或free,因为malloc也是用全局链表来管理堆的

调用了标准I/O库函数,因为很多实现都是使用全局数据结构

六、volatile

C语言中的关键字

作用:保证内存空间的可见性

main执行流中并不会对flag进行修改,而是不断地进行循环检查

编译器优化级别较高时会把flag直接优化成register到寄存器当中

CPU的计算方式

  • 算术运算:加减乘除取模
  • 逻辑运算:或与异或取反真假

编译器的优化级别,默认优化级别为O0

实验一:没有加volatile关键字

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>

int flag = 0;

void handler(int signum)
{
    std::cout << "更改全局变量, " << flag << "-> 1" << std::endl;
    flag = 1;
}

int main()
{
    signal(2, handler);
    
    while(!flag);
    std::cout << "process quit normal" << std::endl;
    return 0;
}

实验现象

实验二:使用volatile关键字修饰

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>

volatile int flag = 0;

void handler(int signum)
{
    std::cout << "更改全局变量, " << flag << "-> 1" << std::endl;
    flag = 1;
}

int main()
{
    signal(2, handler);

    while(!flag);
    std::cout << "process quit normal" << std::endl;
    return 0;
}

实验现象

七、SIGCHLD信号

子进程退出,父进程等待,回收子进程

子进程在退出前会给父进程发送SIGCHLD信号

实验:子进程退出时给父进程发生信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>

void Say(int num)
{
    std::cout << "father get a signal: " << num << std::endl;
}

int main()
{
    //父进程
    signal(SIGCHLD, Say);
    pid_t id = fork();
    if(id == 0)
    {
        std::cout << "I am child, exit" << std::endl;
        sleep(3);
        exit(3);
    }
    waitpid(id, nullptr, 0);
    std::cout << "I am father, exit" << std::endl;

    return 0;
}

实验现象

实验:使用信号捕捉的方式回收子进程

如果使用阻塞调用,父进程会报持访问子进程的状态

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>

void WaitAll(int num)
{
    while (true)
    {
        pid_t n = waitpid(-1, nullptr, 0);
        if (n == 0)
        {
            break;
        }
        else if (n < 0)
        {
            std::cout << "waitpid error " << std::endl;
            break;
        }
    }
    std::cout << "father get a signal: " << num << std::endl;
}

int main()
{
    signal(SIGCHLD, WaitAll);

    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            sleep(3);
            if(i <= 6)
            {
                std::cout << "I am child, exit" << std::endl;
                exit(3);   
            }
            else
            {
                pause();
            }
        }
    }

    while (true)
    {
        std::cout << "I am father, exit" << std::endl;
        sleep(1);
    }

    return 0;
}

实验现象:父进程发生阻塞

所以需要用非阻塞等待的方式回收子进程

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>

void WaitAll(int num)
{
    while (true)
    {
        // waitpid使用非阻塞轮询
        // 如果使用阻塞调用,waitpid会阻塞
        pid_t n = waitpid(-1, nullptr, WNOHANG);
        if (n == 0)
        {
            break;
        }
        else if (n < 0)
        {
            std::cout << "waitpid error " << std::endl;
            break;
        }
    }
    std::cout << "father get a signal: " << num << std::endl;
}

int main()
{
    signal(SIGCHLD, WaitAll);

    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            if(i < 6)
            {
                std::cout << "I am child, exit" << std::endl;
                exit(3);   
            }
            else
            {
                pause();
            }
        }
    }

    while (true)
    {
        std::cout << "I am father, exit" << std::endl;
        sleep(1);
    }

    return 0;
}

实验现象

将SIGCHLD的处理动作设置为SIG_IGN就可以自动回收子进程

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>

int main()
{
    signal(SIGCHLD, SIG_IGN);

    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            sleep(3);
            std::cout << "I am child, exit" << std::endl;
            exit(3);
        }
    }

    while (true)
    {
        std::cout << "I am father, exit" << std::endl;
        sleep(1);
    }

    return 0;
}

实验现象

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐