前言
这一节内容我没怎么认真写,就是纯当草草过了一遍,这部分不是很重要当然能掌握肯定更好。
更多的是有个印象然后知道遇到这样的问题能回想起来知道怎么解决即可(虽然不太可能遇到)。
信号量
实现PV操作
P:测试并加锁,sem <= 0 会阻塞,但如果sem > 0的话 就 --sem
V:解锁,即++sem
实现PV操作可以分为几步,首先定义PV操作然后是调用PV操作。
定义PV操作需要用到的系统调用为:
对该系统调用的一些解释:
信号量的简单使用:
SEM_UNDO的作用是在进程终止的时候,把减去的资源给加回来,这个在下面的生产者消费者问题中有所体现。
生产者消费者问题
在考研课程还有什么哲学家进餐问题乱七八糟的,这些只对考研有用,在实际工作生产中,只要懂消费者生产者问题即可,所以这个很重要,利用之前学过的知识很容易写出这样的代码:
#include <43func.h>
int main()
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
ERROR_CHECK(shmid, -1, "shmget");
int *p = (int *)shmat(shmid, NULL, 0);
ERROR_CHECK(p, (void *)-1, "shmat");
p[0] = 10; // p[0] 表示仓库的个数
p[1] = 0; // p[1] 表示商品的个数
int semid = semget(1000, 1, IPC_CREAT | 0600);
ERROR_CHECK(semid, -1, "semget");
int ret = semctl(semid, 0, SETVAL, 1);
ERROR_CHECK(ret, -1, "semctl SETVAL");
ret = semctl(semid, 0, GETVAL);
ERROR_CHECK(ret, -1, "semctl GETVAL");
printf("semval = %d\n", ret);
struct sembuf P, V;
P.sem_num = 0; //下标
P.sem_op = -1; //对资源的影响
P.sem_flg = SEM_UNDO;
V.sem_num = 0;
V.sem_op = 1;
V.sem_flg = SEM_UNDO;
if (fork() == 0)
{
while (1)
{
semop(semid, &P, 1);
if (p[0] > 0)
{
printf("before produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
--p[0];
++p[1];
printf("after produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
}
semop(semid, &V, 1);
// sleep(1);
}
}
else if (fork() == 0)
{
while (1)
{
semop(semid, &P, 1);
if (p[0] > 0)
{
printf("before produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
--p[0];
++p[1];
printf("after produce, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
}
semop(semid, &V, 1);
// sleep(1);
}
}
else
{
while (1)
{
semop(semid, &P, 1);
if (p[1] > 0)
{
printf("before consume , space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
--p[1];
++p[0];
printf("after consume, space = %2d, good = %2d, total = %d\n", p[0], p[1], p[0] + p[1]);
}
semop(semid, &V, 1);
// usleep(100000);
}
wait(NULL);
}
shmdt(p);
shmctl(shmid, IPC_RMID, NULL);
}
写这种PV操作时一定要分析清楚临界资源有哪些,一定要把所有对共享资源的访问保护到。
但是还是老话,实际工作当中我们不会用这样的方式,会用一些更加高效的手段,这后面再说。
消息队列
所谓消息队列其实有两种,我们这里说的是在进程间通信的消息队列,是一种IPC机制,而另一种消息队列是广义消息队列,它是一种网络通信的中间件,比如RacketMq之类的。
这里说的消息队列和之前说的管道其实非常相似,区别在于消息队列可以保留消息的边界。
什么意思?
先来说管道没有保留消息边界的含义:
比如我们用write系统调用写了两句消息,“你好”,“坏人”,通过管道传输给另一端,因为管道是流式消息,它根据先进先出原则在管道另一头的消息是:“人”“坏”“好”“你”,在管道另一边我们可以选择直接全部取出来也可以选择只读几个字,假如读三个字的话:“你好坏”,最后只剩个“人”了,消息含义就变了,
这就是由于管道没有保留消息边界所造成的:
后面学习网络编程时TCP也是流式传送数据的。
消息队列的做法则不同,还是以上面说的为例,发送方发送你好坏人,每一条消息都会以数据包的形式发送,会一个包一个包的发送给另一端,另一端自然只能一个包一个包的接收,这样就可以保留消息边界:
后面学习网络编程时UDP也是按数据包的形式传送的。
我们通过msgget系统调用来创建消息队列,注意介绍的依然是System V标准的系统调用,因为Posix标准的太难用:
对该系统调用的一些解析(注意消息队列是先进先出的):
代码简单测试:
发送数据的程序:
#include <43func.h>
typedef struct msgbuf{
long mtype;
char mtext[256];
} myMsg_t;
int main(){
int msqid = msgget(1000,IPC_CREAT|0600);
ERROR_CHECK(msqid,-1,"msgget");
myMsg_t msg1;//Huangxiaoming
myMsg_t msg2;//Wuyifan
myMsg_t msg3;//Caixukun
msg1.mtype = 1;
strcpy(msg1.mtext,"Ganenguoqusuoyou,weilairenshijiaren");
msg2.mtype = 2;
strcpy(msg2.mtext,"skr skr~");
msg3.mtype = 3;
strcpy(msg3.mtext,"jinitaimei");
msgsnd(msqid,&msg1,strlen(msg1.mtext), 0);
msgsnd(msqid,&msg2,strlen(msg2.mtext), 0);
msgsnd(msqid,&msg3,strlen(msg3.mtext), 0);
puts("send over");
}
接收数据的程序:
#include <43func.h>
typedef struct msgbuf{
long mtype;
char mtext[256];
} myMsg_t;
int main(){
int msqid = msgget(1000,IPC_CREAT|0600);
ERROR_CHECK(msqid,-1,"msgget");
long type;
printf("who are you? 1 huangxiaoming 2 wuyifan 3 caixukun\n");
scanf("%ld",&type);
myMsg_t msg;
memset(&msg,0,sizeof(msg));
//msgrcv(msqid,&msg,sizeof(msg.mtext),type,0);
//msgrcv(msqid,&msg,sizeof(msg.mtext),0,0);
int ret = msgrcv(msqid,&msg,sizeof(msg.mtext),0,IPC_NOWAIT);
ERROR_CHECK(ret,-1,"msgrcv");
printf("you are %ld, msg = %s\n", type, msg.mtext);
}
proc文件系统
我们来介绍一下这个东西,我们在根目录下打开可以看见该文件目录,cd进行展示会发现很多文件大小都是0:
这是因为proc文件系统不是一个真正的磁盘系统,而是一个伪文件系统。
proc是process的缩写,所以proc文件系统的内容是操作系统的运行状态在文件系统中的映射。
这样做的好处是可以像修改文件一样去修改操作系统的属性。
proc文件目录下面的数字开头的内容其实对应的就是OS中的每一个进程,数字代表进程的PID号。
我们还可以进入这个sys文件目录下,即系统进程下去查看很多信息:
这里面有个kernel内核信息,我们可以在内核文件夹里去修改很多的内容,比如改共享内存的内容啊改消息队列的内容啊之类的。
信号
信号是一种软件层面的异步事件机制。
信号可能是进程发给进程的,也有可能是操作系统发给进程的。
而与之对应的在硬件层面的异步事件机制是中断。
使用man 7 signal命令查看信号手册:
信号默认行为
从上到下分别是:终止、忽略、终止并生成core、暂停、恢复。
可以看见上面的每个信号都会对应一个默认的行为。
我们学习这一节的内容,就是为了实现更改默认的信号行为,所以上面的内容只是铺垫。
信号产生的时机
进程或者操作系统或者硬件产生信号,然后递送到目标进程,中间会有一个传送的时间间隔,我们接下来的事情不会去管产生信号的行为,而是去修改传送信号的时间以及目标进程递送信号的行为。
当信号产生时
更改默认的信号行为
现在我们试图让信号递送时不再执行默认操作,而是调用一个函数。
这里我们可以用signal系统调用看看,用man 2 signal命令查看其man手册:
这个系统调用的作用是用来注册一个信号处理行为,注册的含义是等到信号到来时才会调用。
简单使用代码测试一下:
低速系统调用
低速系统调用是指可能陷入永久等待的系统调用。
signal特点之一就是一次注册,永久生效,如何改变这种永久生效的效果为让注册只生效一次?
可以使用SIG_DFL,设置其为默认的行为模式:
特点之二时,递送A时,会将A假如mask,其它信号不会加入mask,且会自动重启低速系统调用。
为了更好的控制上述特点,我们可以使用sigaction来替代signal:
虽然sigaction可以更好的控制信号,但是也更加复杂了,主要体现在其参数结构体的设计上:
简单测试:
#include <43func.h>
void sigFunc(int num){
printf("before, num = %d\n", num);
sleep(3);
printf("after, num = %d\n", num);
}
void sigFunc3(int num, siginfo_t *siginfo , void * p){
printf("num = %d\n", num);
printf("sender pid = %d\n", siginfo->si_pid);
}
int main(){
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_handler = sigFunc;
//act.sa_sigaction = sigFunc3;
//act.sa_flags = SA_RESTART|SA_SIGINFO|SA_RESETHAND;
//act.sa_flags = SA_RESTART|SA_NODEFER;
act.sa_flags = SA_RESTART;
sigaddset(&act.sa_mask,SIGQUIT);
int ret = sigaction(SIGINT,&act,NULL);
ERROR_CHECK(ret,-1,"sigaction");
//ret = sigaction(SIGQUIT,&act,NULL);
//ERROR_CHECK(ret,-1,"sigaction");
char buf[100] = {0};
read(STDIN_FILENO,buf,sizeof(buf));
printf("buf = %s\n", buf);
//while (1)
//{
//}
}
sa_mask
sa_mask用来指定递送过程中的额外屏蔽信号
sigprocmask实现全程阻塞
简单测试:
运行结果:
获取pending集合
pause等待信号
kill发送信号
简单代码测试:
运行结果:
使用raise系统调用可以给自己发信号:
简单的代码测试:
运行效果:
在实现有序退出时该函数还是有用的。
alarm系统调用:定闹钟
可以简单的使用一下:
时钟
简单使用: