信号
信号通信,其实就是内核向用户空间进程发送信号,只有内核才能发信号,用户空间进程不能发送信号。
关于信号指令的查看:kill -l
例如我们之前使用的kill -9 pid用于杀死一个进程
使用一个死循环
成功发送kill -9指令,杀死该进程
信号通信的框架
信号的发送(发送信号进程):kill、raise、alarm
信号的接收(接收信号进程) : pause()、 sleep、 while(1)
信号的处理(接收信号进程) :signal
相关信号的含义
1.信号的发送(发送信号进程)
kill:
所需头文件:
#include <signal.h>
#include <sys/types.h>
函数原型:int kill(pid_t pid, int sig);
参数:
函数传入值:pid
正数:要接收信号的进程的进程号
0:信号被发送到所有和pid进程在同一个进程组的进程
‐1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
sig:信号
函数返回值:
成功 0
出错 ‐1
所以我们可以使用一个封装的函数来使用信号命令
也可以发送信号杀死进程
2.raise: 发信号给自己 == kill(getpid(), sig)
所需头文件: #include <signal.h>
#include <sys/types.h>
函数原型: int raise(int sig);
参数:
函数传入值:sig:信号
函数返回值:
成功 0
出错 ‐1
我们先写一个简单的例子
成功对自己发送了信号
再来举个例子
父子进程,对子进程发送暂停信号(SIGTSTP)
程序运行8秒之内,父进程处于s+状态(睡眠),子进程处于T+状态(暂停)
8秒之后,父进程处于R+状态(运行)(因为while(1)循环),子进程仍旧处于T+状态
我们对代码进行修改
父进程S+状态,子进程T+状态
父进程R+状态,子进程Z+(变为僵尸进程),因为父进程想要回收,但是子进程被命令杀死,父进程还处于运行态无法进行回收,子进程变为僵尸进程。
我们在父进程执行循环之前加上阻塞函数等待子进程并回收
8秒之前还是父进程S+状态,子进程T+状态
而在8秒之后,父进程开始运行循环,进入R+(运行态),子进程成功被回收
3.alarm : 发送闹钟信号的函数
alarm 与 raise 函数的比较:
相同点:
让内核发送信号给当前进程
不同点:
alarm 只会发送SIGALARM信号
alarm 会让内核定时一段时间之后发送信号, raise会让内核立刻发信号
所需头文件#include <unistd.h>
函数原型 unsigned int alarm(unsigned int seconds)
参数:
seconds:指定秒数
返回值:
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则 返回上一个闹钟时间的剩余 时间,否则返回0。
出错:‐1
举个简单例子
信号的接收
接收信号的进程,要有什么条件:要想使接收的进程能收到信号,这个进程不能结束 :
sleep
pause:进程状态为S(休眠状态)
函数原型 int pause(void);
函数返回值
成功:0,出错:‐1
3.信号的处理
收到信号的进程,应该怎样处理? 处理的方式:
1.进程的默认处理方式(内核为用户进程设置的默认处理方式)
A:忽略B:终止进程C: 暂停
2.自己的处理方式: 自己处理信号的方法告诉内核,这样你的进程收到了这个信号就会采用你自己的的处理方式、
所需头文件 #include <signal.h>
函数原型 void (*signal(int signum, void (*handler)(int)))(int);
函数传入值
signum:指定信号
handler
SIG_IGN:忽略该信号。 //ignore
SIG_DFL:采用系统默认方式处理信号
自定义的信号处理函数指针
函数返回值
成功:设置之前的信号处理方式
出错:‐1
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函 数;这个函数的返回值是一个函数指针。
上面函数的执行顺序
- 程序开始执行:
- 程序进入
main
函数。
- 程序进入
- 设置信号处理函数:
- 调用
signal(SIGALRM, myfun);
,这告诉操作系统当接收到SIGALRM信号时,应该调用myfun
函数来处理它。
- 调用
- 打印"before alarm":
- 执行
printf("before alarm\n");
,在控制台上输出before alarm
。
- 执行
- 设置alarm:
- 调用
alarm(7);
,设置一个7秒后的定时器,到时后向进程发送SIGALRM信号。
- 调用
- 打印"after alarm":
- 立即执行
printf("after alarm\n");
,在控制台上输出after alarm
。此时,虽然alarm
已经设置,但SIGALRM信号尚未发送,因此myfun
函数尚未被调用。
- 立即执行
- 进入主循环:
- 开始执行
while (i < 10)
循环。此时,i
的初始值为0。 - 在循环的每次迭代中,
i
递增,然后调用sleep(1);
暂停1秒。 - 接着,打印当前
i
的值(例如process 1
、process 2
等)。
- 开始执行
- SIGALRM信号发送:
- 大约7秒后(从
alarm(7);
调用开始计时),操作系统向进程发送SIGALRM信号。 - 由于之前已经通过
signal(SIGALRM, myfun);
设置了信号处理函数,因此myfun
函数被调用。
- 大约7秒后(从
- 执行
myfun
函数:myfun
函数内部有一个局部变量i
(注意,这与main
函数中的i
不同),它从0开始,并在循环中递增到9。- 在
myfun
函数的循环中,每次迭代都会打印出信号编号(SIGALRM的编号,通常为14,但这里使用signum
变量表示)和循环计数器的值(局部于myfun
的i
)。 - 每次打印后,
myfun
函数中的i
递增,并调用sleep(1);
暂停1秒。
myfun
函数完成:- 当
myfun
函数中的i
达到10时,循环结束,myfun
函数返回。
- 当
- 主循环继续:
- 当
myfun
函数返回时,主循环(在main
函数中)继续执行。由于main
函数中的i
在myfun
函数执行期间也在递增,因此主循环将继续执行直到其i
也达到10。
- 当
- 程序结束:
- 当
main
函数中的i
达到10时,while
循环结束,main
函数返回0,程序正常结束
- 当
使用SIG_IGN函数
使用SIG_DFL函数
信号父子进程之间的通讯
关于下面的函数,几个注意点:
为了避免子进程成为僵尸进程,我们需要对其进行资源回收,但是如果在调用第一个函数signal之后就回收,会导致下面的代码不能按照预期的进行运行
exit()也是一个信号指令,
成功调用signal1函数,目的是使用wait(NULL)进行阻塞,从而达到回收子进程回收资源的目的
子进程回收资源成功
信号灯
信号灯集合(可以包含多个信号灯)IPC对象是一个信号的集合(多个信号量)
semaphore
函数原型:
int semget(key_t key, int nsems, int semflg);
//创建一个新的信号量或获取一个已经存在的信号量的键值。
所需头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数参数:
key:和信号灯集关联的key值
nsems: 信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限
函数返回值:
成功:信号灯集ID
出错:‐1
函数原型:
int semctl ( int semid, int semnum, int cmd,…union semun arg(不是地址));
//控制信号量,删除信号量或初始化信号量
所需头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数参数:
semid:信号灯集ID
semnum: 要修改的信号灯编号
cmd :
GETVAL:获取信号灯的值
SETVAL:设置信号灯的值
IPC_RMID:从系统中删除信号灯集合
函数返回值:
成功:0
出错:‐1
删除成功,这里删除的是最后一个生成的信号灯。
PV操作
基本概念
- 信号量(Semaphore):一个整型变量,用于表示资源的数量。信号量的值大于0时,表示可用资源的数量;值为0时,表示没有可用资源;值为负数时,其绝对值表示等待该资源的进程数。
- P操作(Wait操作):将信号量的值减1,表示请求分配一个资源。如果操作后信号量的值小于0,则表示没有可用资源,进程将被阻塞进入等待队列。
- V操作(Signal操作):将信号量的值加1,表示释放一个资源。如果操作前信号量的值小于0,则表示有进程在等待该资源,系统将唤醒等待队列中的一个进程。
int semop(int semid ,struct sembuf *sops ,size_t nsops);
//用户改变信号量的值。也就是使用资源还是释放资源使用权
包含头文件:
include <sys/sem.h>
参数:
semid : 信号量的标识码。也就是semget()的返回值
sops是一个指向结构体数组的指针。
struct sembuf
{
unsigned short sem_num;//信号灯编号;
short sem_op;//对该信号量的操作。‐1 ,P操作,1 ,V操作
short sem_flg;0阻塞,1非阻塞
};
sem_op : 操作信号灯的个数
//如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负 数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用 权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
这里我们进行PV操作实现父子进程之间的通信
对代码进行了修改,如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// 定义信号量的宏,用于读写操作
#define SEM_READ 0
#define SEM_WRITE 1
// semun 联合体用于 semctl 函数的 SETVAL 操作
union semun {
int val; // 用于设置信号量的值
struct semid_ds *buf;
unsigned short *array;
};
// P 操作(等待信号量)
void Poperation(int index, int semid) {
struct sembuf sop;
sop.sem_num = index; // 信号量的索引
sop.sem_op = -1; // P 操作,信号量减 1
sop.sem_flg = 0; // 操作标志,0 表示默认
if (semop(semid, &sop, 1) == -1) {
perror("semop"); // 如果 semop 失败,打印错误信息
exit(1); // 退出程序
}
}
// V 操作(释放信号量)
void Voperation(int index, int semid) {
struct sembuf sop;
sop.sem_num = index; // 信号量的索引
sop.sem_op = 1; // V 操作,信号量加 1
sop.sem_flg = 0; // 操作标志,0 表示默认
if (semop(semid, &sop, 1) == -1) {
perror("semop"); // 如果 semop 失败,打印错误信息
exit(1); // 退出程序
}
}
int main() {
key_t key; // 用于生成唯一的标识符
int semid, shmid; // 信号量和共享内存段的标识符
char *shmaddr; // 指向共享内存段的指针
pid_t pid; // 进程标识符
// 使用 ftok 生成唯一的 key
key = ftok("123", 65);
if (key == (key_t)-1) {
perror("ftok");
exit(1);
}
// 创建信号量集
semid = semget(key, 2, IPC_CREAT | 0755);
if (semid < 0) {
perror("semget");
return -2;
}
// 初始化信号量
union semun myun;
myun.val = 0;
semctl(semid, SEM_READ, SETVAL, myun); // 初始化读信号量为 0
myun.val = 1;
semctl(semid, SEM_WRITE, SETVAL, myun); // 初始化写信号量为 1
// 创建共享内存段
shmid = shmget(key, 1024, IPC_CREAT | 0755);
if (shmid < 0) {
perror("shmget");
return -3;
}
// 创建子进程
pid = fork();
if (pid == 0) { // 子进程
while (1) {
// 附加共享内存段
shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat");
exit(1);
}
// 等待读信号量
Poperation(SEM_READ, semid);
// 读取并打印共享内存内容
printf("Child: get shared memory is %s\n", shmaddr);
// 释放写信号量
Voperation(SEM_WRITE, semid);
// 分离共享内存段
shmdt(shmaddr);
// 暂停一秒,避免过快循环
sleep(1);
}
}
else if (pid > 0) { // 父进程
char input[32];
while (1) {
// 获取用户输入
printf("Parent: please input to shared memory (q to quit): ");
if (fgets(input, sizeof(input), stdin) == NULL) {
perror("fgets");
break; // 如果读取输入失败,则退出循环
}
// 检查是否输入了退出命令
if (input[0] == 'q' && input[1] == '\n') {
break; // 退出循环
}
// 附加共享内存段
shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat");
exit(1);
}
// 等待写信号量
Poperation(SEM_WRITE, semid);
// 将输入的数据写入共享内存
// 注意:这里直接写入可能不安全,因为fgets包含换行符,并且没有检查缓冲区溢出
// 这里为了简化,我们直接写入,但在实际应用中应该更谨慎
strncpy(shmaddr, input, sizeof(input) - 1); // 减去1以排除可能的换行符
// 释放读信号量,允许子进程读取
Voperation(SEM_READ, semid);
// 分离共享内存段
shmdt(shmaddr);
// 可以在这里添加其他逻辑,如处理错误或进行其他任务
}
// 父进程退出前,可以清理资源(可选)
// 但在这个例子中,由于子进程将无限循环,除非父进程被外部方式终止,
// 否则通常不会执行到这里的清理代码
// 注意:在实际应用中,你可能需要等待子进程结束,或者发送信号来优雅地终止它
// 例如,使用 waitpid 或 kill 函数
} else {
// 如果 fork 失败
perror("fork");
exit(1);
}
// 清理信号量和共享内存(这部分代码在实际应用中可能需要放在更合适的位置)
// 例如,在父进程确定子进程已经终止之后
// 但在这个例子中,由于子进程是无限循环的,所以这里不执行清理
// ...
return 0;
}