目录
练习2:使用无名管道实现父子进程间的通信(子进程->父进程)
5.msgctl( )函数 ( 消息队列控制,包括获得属性、删除消息队列 )
一、内容概括
·匿名管道(PIPE)和有名管道(FIFO)【数据传输】:
管道提供了进程间通信消息传递的实体,其原型来自于数据结构的“队列”。无名管道用于具有亲缘关系的进程(例如父子进程,兄弟进程),而有名管道则允许不具有亲缘关系的进程使用。
·共享内存【数据共享】:
最有效的进程通信方式。它使得多个进程共享一块内存空间,不同进程间可以实时观察到其他进程的数据更新。不过使用该方式需要某种同步与互斥机制。
·消息队列【数据传输】:
消息队列是消息所构成的链表,包括POSIX消息队列与系统V消息队列两种。消息队列克服了管道与信号两种通信方式中信息量有限的缺点。
·信号量【进程控制】:
主要作为进程间以及同一进程的不同线程间的同步与互斥手段。
·进程信号
信号是在软件层面上对中断的一种模拟机制,用于通知进程某个事件发生。
二、匿名管道(PIPE)和有名管道(FIFO)【数据传输】
管道:是内核当中的一块内存,操作系统为进程间通信所创建的缓冲区
匿名管道PIPE
1.匿名管道的特性:
2.创建匿名管道 pipe()函数
所需头文件:#include<unistd.h>
函数原型: int pipe(int fd[2])
函数参数: fd[2]:包含两个文件描述符的数组,其中fd[0]固定用于读管道,fd[1]固定用于写管道
函数返回值:
成功:0
失败:-1
创建管道使用pipe()函数,而其余的操作诸如读取管道read()、写入管道write()、关闭管道close()函数与文件IO的函数使用方式相同。
那么如何使用匿名管道实现父子进程间的通信呢?
由于匿名管道具有固定的读端与写端,因此,如果父子进程需要使用无名管道进行通信,可以进行以下操作:
父进程->子进程:父进程对自己的fd[1]执行写操作,数据流入管道内,然后子进程对自己的fd[0]执行读操作,得到管道内数据
子进程->父进程:子进程对自己的fd[1]执行写操作,数据流入管道内,然后父进程对自己的fd[0]执行读操作,得到管道内数据
无名管道的工作方式是半双工方式,即在一个进程内要么读管道,要么写管道,无法同时进行读写操作。也就是说,在同一时刻内,要么父进程写数据、子进程读数据(父进程->子进程),要么子进程写数据、父进程读数据(子进程->父进程),数据流动方向唯一,不能同时存在两个数据流动方向。在使用时,对于该进程内未使用的文件描述符应当关闭。
练习:创建一个匿名管道进行读写
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe");
return 0;
}
//fd[0] : 读-文件描述符
//fd[1] : 写-文件描述符
//写
write(fd[1], "linux-66", 8);
char buf[1024] = {0};
read(fd[0], buf, sizeof(buf) - 1);
printf("buf [%s]\n", buf);
while(1)
{
sleep(1);
}
return 0;
}
练习2:使用无名管道实现父子进程间的通信(子进程->父进程)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
int n;
int fd[2];
pid_t pid = fork();
char buf[1024] = { 0 };
if( pipe(fd) < 0 )//创建一个无名管道
{
perror("cannot create a pipe");
exit(0);
}
if( pid < 0 )//创建子进程
{
perror("can not fork");
exit(0);
}
else if( pid == 0)//子进程
{
printf("i am child \n");
close(fd[0]);//关闭该进程内的fd[0](读端),保留fd[1](写端),即接下来对该管道进行写操作
strcpy(buf,"Helloworld\n"); //将字符串赋值到字符数组中
write(fd[1],buf,strlen(buf)); //将数组里的数据写入管道中
close(fd[1]);//管道使用完毕,关闭fd[1]
}
else//父进程
{
sleep(1);//保证子进程先写数据
printf("i am father \n");
close(fd[1]);//关闭该进程内的fd[1](写端),保留fd[0](读端),即接下来对该管道进行读操作
n = read( fd[0] , buf , sizeof(buf) - 1); //fd[0]从无名管道中读取数据,读入到buf 数组中,n 为返回的字符个数
printf("father read %d characters, buf is:%s", n , buf );
close(fd[0]);//管道使用完毕,关闭fd[0]
waitpid(pid,NULL,0);//父进程等待回收子进程
}
return 0;
}
3. 管道的方向:
程序中使用管道的时候,关闭了父进程的写端与子进程的读端,相当于强行规定了管道的数据流动方向(子进程->父进程)。那么如果我们想复用该管道传输数据,实现“父进程->子进程”该怎么办呢?
非常遗憾,我们无法改变已经确定数据传输方向的管道的方向,即无法复用管道实现“父进程->子进程”的功能。那么管道为什么必须有方向呢?
实际上,管道方向算是一个历史遗留问题。管道通信是UNIX系统内最古老的通信方式。在早期的内核代码中,由于技术受限以及硬件性能不足,管道是必须确定方向的。现在的操作系统虽然已经足够强大,但是“管道必须确定传输方向”还是被保留下来。出于内核移植性的考虑,我们在使用管道的时候也必须确定管道的数据传输方向。
4.更改对应文件描述符为非阻塞状态
5.创建有名管道(mkfifo)
所需头文件:#include<sys/types.h>
#include<sys/stat.h>
函数原型: int mkfifo(const char *pathname, mode_t mode)
函数参数:
pathname 要创建的有名管道的路径名与文件名
mode 创建的有名管道的文件权限码,通常用八进制数字表示
函数返回值:
成功:0
失败:-1
特性:
练习1:创建一个有名管道文件
#include <stdio.h>
#include <sys/stat.h>
int main()
{
int ret = mkfifo("./CFIFO", 0664);
if(ret < 0)
{
perror("mkfifo");
return 0;
}
//open
//read
//write
//close
return 0;
}
三、共享内存【数据共享】:
1、原理
2.创建共享内存&通信流程
1.现在物理内存中开辟一段空间
2.各个通信通过页表结构将内存映射到自己的虚拟地址空间当中的共享区
3.各个进程之间的通信是通过修改自己虚拟地址空间当中的共享区的地址来完成的
3.特性
不同的进程对共享内存区域进行读的时候,并不会抹除物流内存当中的值
共享内存的使用分为两个步骤:第一步是创建共享内存,使用shmget()函数从内存中获取一块共享内存区域;第二步是映射共享内存,也就是使用shmat()把申请的共享内存区映射到具体的进程空间中。除此之外,还可以使用shmdt()函数撤销映射操作。
我们可以使用ipcs命令查看进程间通信的状态。
4.shmget()函数(创建共享内存)
所需头文件: #include<sys/shm.h>
函数原型:int shmget( key_t key , size_t size , int shmflg )
函数参数:
key 共享内存的键值,其他进程通过该值访问该共享内存,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有共享内存
size 申请的共享内存段的大小
shmflg
函数返回值:
成功:共享内存段的标识符(非负整数)
失败:-1
5. shmat()函数(将进程附加到共享内存上)
所需头文件: #include<sys/shm.h>
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg)
函数参数:
shmid 要映射的共享内存区标识符(即shmget()函数的返回值)
shmaddr 将共享内存映射到的指定内存地址,如果为NULL则会自动分配到一块合适的内存地址
shmflg SHM_RDONLY表示共享内存为只读,0(默认值)表示共享内存可读可写
函数返回值:
成功:被映射的内存地址
失败:-1
6. shmdt()函数(从共享内存中分离进程)
所需头文件:#include<sys/shm.h>
函数原型:int shmdt(const void *shmaddr)
函数参数:shmaddr 需要解除映射的共享内存地址
函数返回值:
成功:0
失败:-1
7. shmctl()函数(共享内存的销毁)
所需头文件:#include<sys/shm.h>
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
函数参数:
shmid 共享内存区标识符(即shmget()函数的返回值)
cmd 需要对共享内存采取的操作。可取值有很多,常用的有:
IPC_STAT 将shmid_ds结构体中的数据设置为共享内存的当前关联值,即用shmid覆盖shmid_ds内的值
IPC_SET 如果进程权限允许,将共享内存的当前关联值设置为shmid_ds中给出的值
IPC_RMID 删除共享内存
buf 该参数是一个shmid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct shmid_ds
{
uid_t shm_perm.uid; /* Effective UID of owner */
uid_t shm_perm.gid; /* Effective GID of owner */
mode_t shm_perm.mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
……
};
函数返回值:
成功:
IPC_INFO或SHM_INFO操作:内核内部记录的有关共享内存段的使用条目
SHM_STAT操作:shmid中指定的共享内存标识符
其他操作:0
失败:-1
练习1:演示4个共享内存函数。
首先创建一块共享内存(在本例中设置为IPC_PRIVATE),之后创建子进程,在父子进程内分别将各自的进程映射到共享内存。
父进程等待用户输入,然后在共享内存内写入"WROTE"字符串连通用户输入数据共同写入共享内存;子进程待共享内存内出现"WROTE"数据后读出用户输入内容。
待数据传输结束后父子进程分别取消映射。最后删除共享内存。
在程序运行期间,多次调用"system("ipcs -m")"命令报告共享内存情况,注意每次报告的情况的异同。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string.h>
#define BUFFERSIZE 2048
int main()
{
pid_t pid;
int shmid;//共享内存标识符
char *shm_addr;
char flag[]="WROTE";
char buff[BUFFERSIZE];
//创建共享内存
if((shmid=shmget(IPC_PRIVATE,BUFFERSIZE,0664))<0)
{
perror("cannot shmget");
exit(0);
}
else
{
printf("Create sharedmemory: %d\n",shmid);
}
system("ipcs -m");//显示当前共享内存情况
pid = fork();//创建进程
if(pid == -1)
{
perror("cannot fork");
exit(0);
}
else if(pid == 0)//子进程
{
//映射共享内存
if((shm_addr=shmat(shmid,NULL,0)) == (void*)-1)//注意-1的写法
{
perror("Child Process: shmat");
exit(0);
}
else
{
//映射成功
printf("Child Process: Attach sharedmemory: %p\n",shm_addr);
}
system("ipcs -m");//显示当前共享内存情况
while(strncmp(shm_addr,flag,strlen(flag)))//等待传输数据
{
printf("Child Process is waiting for data……\n");
sleep(3);
}
strcpy(buff,shm_addr + strlen(flag));//获取数据,从WROTE后面开始读取
printf("Child Process read %s\n",buff);
//解除共享内存映射
if(shmdt(shm_addr)<0)
{
perror("Child Process: shmdt");
exit(0);
}
else
{
//解除映射成功
printf("Child Process Deattach sharedmemory\n");
}
system("ipcs -m");//显示当前共享内存情况
//删除共享内存
if(shmctl(shmid,IPC_RMID,NULL)==-1)
{
perror("Child Process: shmctl(IPC_RMID)\n");
exit(0);
}
else
{
printf("Delete sharedmemory\n");
system("ipcs -m");//显示当前共享内存情况
}
}
else//父进程
{
//映射共享内存
if((shm_addr=shmat(shmid,NULL,0)) == (void*)-1)//注意-1的写法
{
perror("Parent Process: shmat");
exit(0);
}
else
{
//映射成功
printf("Parent: Attach sharedmemory: %p\n",shm_addr);
}
sleep(1);//让子进程先运行
printf("\nInput string:");
fgets(buff,BUFFERSIZE,stdin);
strcpy(shm_addr,flag);//写入共享内存WROTE
strncpy(shm_addr + strlen(flag),buff,strlen(buff));//写入共享内存用户输入数据
//解除共享内存映射
if(shmdt(shm_addr)<0)
{
perror("Parent Process: shmdt");
exit(0);
}
else
{
//解除映射成功
printf("Parent Process Deattach sharedmemory\n");
}
system("ipcs -m");//显示当前共享内存情况
waitpid(pid,NULL,0);//等待回收子进程
printf("Finished\n");
}
return 0;
}
四、消息队列【数据传输】
1.消息队列的特性
·先进先出,底层的是现实链表,内核当中创建
·在队列中每一个元素都有自己的类型,类型之间有一个优先级的概念
·每一个几点最大的发送自己数量:8K
·队列中所有消息的长度之和:16384
·系统当中最大的队列数:2379
使用消息队列通常需要创建/打开消息队列、添加消息、读取消息、控制消息队列四种操作
2.msgget( )函数(创建/打开消息队列)
所需头文件: #include<sys/msg.h>
函数原型:int msgget(key_t key,int msgflg)
函数参数:
key 消息队列的键值,其他进程通过该值访问该消息队列,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有消息队列
msgflg
IPC_CREAT
IPC_CREAT | IPC_EXCL
按位或上权限
函数返回值:
成功:消息队列ID
失败:-1
3.msgsnd( ) (向消息队列中添加消息)
所需头文件: #include<sys/msg.h>
函数原型: int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
函数参数:
msqid 消息队列标识符(即msgget( )函数的返回值)
msgp 指向消息队列的结构体指针,必须使用地址传递的方式传参。该结构体的类型如下:
struct msgbuf
{
long mtype;//消息类型,必须大于0
char mtext[n];//消息正文
};
msgsz 消息正文的字节数,必须与第二个参数的消息正文数据长度一致
msgflg
IPC_NOWAIT 若消息无法立即发送则立即返回
0 若消息无法立即发送则阻塞等待消息发送成功
函数返回值:
成功:0
失败:-1
4.msgrcv( )函数(从消息队列中提取消息)
所需头文件: #include<sys/msg.h>
函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
函数参数:
msqid 消息队列标识符(即msgget()函数的返回值)
msgp 指向消息队列的结构体指针,必须使用地址传递的方式传参。该结构体的类型如下:
struct msgbuf
{
long mtype;//消息类型,必须大于0
char mtext[n];//消息正文
};
msgsz 消息正文的字节数,必须与第二个参数的消息正文数据长度一致
msgtyp
0 接收消息队列中第一个消息
大于0 接收消息队列中第一个值为msgtyp的消息
小于0 接收消息队列中具有小于或等于msgtyp绝对值的最小mtype值的第一条消息
msgflg
MSG_NOERROR 若返回的消息比msgsz字节多,则消息会截断到msgsz字节,且不通知消息发送进程
IPC_NOWAIT 若消息队列中无对应类型的消息接收则立即返回
0 阻塞等待直至接收到一条相应类型的消息为止
函数返回值:
成功:0
失败:-1
5.msgctl( )函数 ( 消息队列控制,包括获得属性、删除消息队列 )
所需头文件: #include<sys/msg.h>
函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf)
函数参数:
msqid 消息队列标识符(即msgget()函数的返回值)
cmd 需要对消息队列采取的操作。可取值有很多,常用的有:
IPC_STAT 读取消息队列的数据结构msqid_ds并将其存储在buf指定的地址中
IPC_SET 设置消息队列中的数据结构msqid_ds中的pic_perm元素的值。这个值来自buf参数
IPC_RMID 从内核中删除消息队列
buf 该参数是一个msqid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct msqid_ds
{
uid_t msg_perm.uid; /* Effective UID of owner */
gid_t msg_perm.gid; /* Effective GID of owner */
……
};
函数返回值:
成功:0
失败:-1
五、信号量【进程控制】:
1.作用:实现了同步和互斥
2.信号量的本质
·信号量的本质是一个计数器 + PCB等待队列
·计数器的含义是对资源的计数,对资源进行+1或者-1操作
3.如何实现一个互斥?
前提条件:信号量当中的资源计数器只有两个取值(0或1),0表示不可用,1表示可用
访问:当一个进程需要访问一个临界资源的时候,先获取信号量,预算信号量当中的计数器的值
预算:对当前信号量当中的计数器值进行-1操作,判断当前信号量当中的值是否小于0
释放:
如果临界资源访问完成,需要结束对临界资源的访问,则需要对信号量当中的计数器进行+1操作。唤醒PCB等待队列当中的进程
PV操作的具体定义如下:
P操作(通过):对信号量减1,若结果大于等于0,则进程继续,否则执行P操作的进程被阻塞等待释放
V操作(释放):对信号量加1,若结果小于等于0,则唤醒队列中一个因为P操作而阻塞的进程,否则不必唤醒进程
六、进程信号
1.信号的基本概念
信号是一个软件中断,打断当中正在进行的进程,让该进程去处理信号的事件
2.信号的种类
62种信号
1~31:不可靠信号
34~64:可靠信号
3.信号的生命周期
信号的产生
4.信号的注册
·非可靠信号:1~31
·可靠信号:34~64
5.信号的注销
·非可靠信号:1~31
·可靠信号:34~64
6.信号的处理方式
1.默认处理
SIG_DEL——>执行一个操作系统定义好的动作(操作系统执行了一个函数)
2.忽略处理
SIG_IGN——>操作系统不会做什么事
例如:僵尸进程:子进程退出的时候,给父进程发送了一个SIGCHID信号,但是操作系统对SIGCHID信号的处理方式为忽略方式,而到时父进程不去处理信号。从而子进程编程僵尸进程。
3.自定义处理(自己定义信号的处理函数)
signal()函数
所需头文件:#include<signal.h>
函数原型:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数参数:
signum 指定信号
handler 指定接收信号后的处理方式
SIG_IGN:忽略信号
SIG_DFL:采用默认方式处理信号
其他:自定义信号处理函数
函数返回值:
成功:以前的信号处理函数
失败:SIG_ERR
练习1:使用signal()函数捕捉信号,并执行相应的信号处理函数。其中SIGINT代表ctrl+c组合键,SIGQUIT代表ctrl+\组合键。
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void handler(int sig_no) //自定义信号处理函数
{
if(sig_no == SIGINT)
{
printf("Got a signal: SIGINT(ctrl+c)\n");
}
else if(sig_no == SIGQUIT)
{
printf("Got a signal: SIGQUIT(ctrl+\\)\n");
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGQUIT,handler);
printf("Waiting for signal SIGINT or SIGQUIT……\n");
pause(); //等待接收信号
return 0;
}
sigaction()函数
7.信号的捕捉流程
8.信号的阻塞
·信号的阻塞不是说信号不能被注册,是不会影响信号更改pengding位图和增加sigquueue节点
·操作系统处理信号的逻辑
当程序从用户态切换到内核态之后,处理do_signal函数的时候,发现收到某个信号,想要处理这个信号之前,先判读block 位图当中对应信号的bit为是否为1
block当中对应bit位为1 :则不处理该信号,sigqueue当中对应的信号的节
block当中对应bit位为0 :则处理该信号
·信号阻塞的时候,并不会干扰信号的注册
非可靠信号,收到多个同样的非可靠信号,只会添加一次sigqueue节点,也就是意味着只会处理一次
可靠信号,收到多个同样的可靠信号,会添加多次sigqueue节点,也就是意味着每一个可靠信号都会被处理