一、进程间通信介绍
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。那么释放的资源可能是其他进程需要的,然而进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
二、管道通信
1.无名管道
无名管道,是 UNIX 系统IPC最古老的形式
(1)特点
1.它是半双工的(即数据只能在一个方向上流动),具有固定读端和写端。
2.它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
3.它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。(其字节大小为0)
(2)无名管道的创建与关闭
无名管道是基于文件描述符的通信方式。当一个管道创建时,它会创建两个文件描述符:fd[0] 、fd[1] 。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道,如下图,这样就构成了一个单向的数据通道:
管道关闭时只需要用 close() 函数将这两个文件描述符关闭即可。
(3)函数原型
#include <unistd.h>//所需头文件
int pipe(int fd[2]);
-
参数:fd ,包含两个元素的整型数组,存放管道对应的文件描述符
-
函数返回值:成功:0,出错:-1
(4)用法说明
用pipe() 函数创建的管道两端处于一个进程中。由于管道主要是用于不同进程间的通信,通常是先创建一个管道,再调用 fork () 函数创建一个子进程,该子进程会继承父进程所创建的管道。需要注意的是,无名管道是单工的工作方式,即进程要么只能读管道,要么只能写管道。父子进程虽然都拥有管道的读端和写端,但是只能使用其中一个,若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。
示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2];//存放文件描符的数组
char readBuf[128];
pid_t pid;
int readSize;
if(pipe(fd) == -1){
printf("creat pipe faild!");
exit(0);
}
pid = fork();
if(pid > 0){//父进程
printf("This is father progress the pid is %d \n",getpid());
close(fd[0]);//关闭父进程的读
write(fd[1],"hello this is father",strlen("hello this is father"));//利用管道向子进程发送信息
wait(NULL);//等待子进程退出
}
if(pid == 0){//子进程
printf("This is child progress the pid is %d \n",getpid());
close(fd[1]);//关闭子进程的写
read(fd[0],readBuf,128);
printf("the message from farherr is %s \n",readBuf);
exit(1);
}
return 0;
}
2.有名管道
有名管道(FIFO)是对无名管道的一种改进
(1)特点
1.它可以使互不相关的两个进程实现彼此通信;
2.该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便;
3.FIFO严格地遵循先进先出规则,对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。有名管道不支持如lseek()等文件定位操作;
(2)函数原型
#include <sys/stat.h>//需要引入的头文件
int mkfifo(const char *pathname, mode_t mode);
- 参数 :
pathname参数:为所创建管道的路径名称
mode 参数:与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。 - 返回值:成功返回0,出错返回-1
(3)有名管道用法
有名管道(FIFO)的创建可以使用 mkfifo() 函数,该函数类似文件中的open() 操作,可以指定管道的路径和访问权限 (用户也可以在命令行使用 “mknod <管道名>”来创建有名管道)。
在创建管道成功以后,就可以使用open()、read() 和 write() 这些函数了。与普通文件一样,对于为读而打开的管道可在 open() 中设置 O_RDONLY,对于为写而打开的管道可在 open() 中设置O_WRONLY。
-
对于读进程
缺省情况下,如果当前FIFO内没有数据,读进程将一直阻塞到有数据写入或是FIFO写端都被关闭。
-
对于写进程
只要FIFO有空间,数据就可以被写入。若空间不足,写进程会阻塞,知道数据都写入为止;
-
关于是否设置阻塞标志:
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO -
示例
writefifo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
int fd;
int n;
char buf[1024] = {0};
if(argc < 2){
printf("输入需要创建或打开的文件名!\n");
exit(1);
}
fd = open(argv[1],O_WRONLY);//以只写的方式打开fifo
if(fd < 0){
printf("opem fail\n");
perror("why");
exit(1);
}
while(1){
printf("输入要发送的内容:");
scanf("%s",buf);
n= write(fd,buf,strlen(buf));//写入fifo的内容
if(n == -1){
perror("write fails");
}
}
return 0;
}
openfifo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
int fd;
int nread;
char buf[128] = {0};
if(argc < 2){
printf("输入需要创建或打开的文件名!\n");
exit(1);
}
if((mkfifo(argv[1], 0600)<0)&& errno != EEXIST){//其中 errno 表示错类型,这种写法可以在文件已经存在时不报错
printf("creat fifo fail\n");
perror("why");
exit(1);
}
fd = open(argv[1],O_RDONLY);
if(fd < 0){
printf("open fail\n");
perror("why");
exit(1);
}
while(1){
nread = read(fd,buf,128);//读取fifo的内容
if(nread == -1){
perror("read fails");
exit(1);
}else if(nread ==0){
printf("none from fifowrite\n");
exit(1);
}else
{
buf[nread] = '\0';//将读出的最后一个字节添加一个结束符
printf("read %d bytes from fifo:%s\n",nread,buf);
}
}
return 0;
}
- 执行结果
写端:
读端:
三、消息队列
1.消息队列相关概念
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识
(1) 消息队列是进程间通信的一种方式,遵循先进先出的原则,保证了时间的顺序性。拥有该消息队列读权限的进程可以从消息队列读出数据,拥有该消息队列写权限的进程可以向消息队列发送数据。
(2)消息作为节点一个一个地存放在消息队列里,可把消息队列比作信箱,消息比作依次顺序存放的信件。地址比作消息类型,内容为消息。支持双向传输,可以使用消息类型区分不同的消息。其实消息队列说白了就是用来存放消息的链表。
(3)消息队列不再局限于父子进程,在任何两个进程间都能通信。
- 特点:
(1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
(2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
(3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
2.消息队列相关函数
#include <sys/msg.h>//需要引入的头文件
(1) msgget函数(消息队列创建)
- 作用:用来创建一个消息队列
- 函数原型:
int msgget(key_t key, int flag);
- 参数:
key:某个消息队列的名字
flag:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
IPC_CREAT//不存在创建,存在就打开
IPC_CREAT|EXCL//不存在就创建,存在出错
- 返回值:成功返回一个非负整数(即该消息队列的标识码),失败返回-1
(2)msgsnd函数(发送消息)
- 作用:发送消息,即把一条消息添加到消息队列中去
- 函数原型;
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 参数:
msqid:由msgget函数返回的消息队列标识码,表示往哪个消息队列发数据
msgp:是⼀个指针,指针指向准备发送的消息(即准备发送的消息的内容)
消息的结构参考:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
参数:
mtype:它必须以⼀个long int⻓整数开始,接收者函数将利⽤这个⻓整数确定消息的类型
mtext:保存消息内容的数组或指针,它必须小于系统规定的上限值
msgsz:消息的大小,即即mtext的大小。
msgflg:0表示阻塞方式,IPC_NOWAIT表⽰队列满不等待,返回EAGAIN错误
返回值:成功返回0,失败返回-1
(3)msgrcv函数(接收消息)
- 作用:从一个消息队列中接收消息
- 函数原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
- 参数:
msqid:由msgget函数返回的消息队列标识码,表示从哪个消息队列拿数据
msgp:⼀个指针,指针指向准备接收的消息(即接收消息的缓冲区)
msgsz:是msgp指向的消息⻓度,这个⻓度不含保存消息类型的那个long int⻓整型,即用sizeof()计算
msgtyp:它可以实现接收优先级的简单形式,即接收消息的类型,即发消息结构体中的mtype
msgflg:控制着队列中没有相应类型的消息可供接收时将要发⽣的事,即0表示阻塞方式, IPC_NOWAIT表⽰队列满不等待,返回EAGAIN错误 - 返回值:成功返回实际放到接收缓冲区里去的字符个数,失败返回-1
(4)msgctl函数(控制消息队列)
- 作用:消息队列的控制函数
- 函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 参数
msqid:由msgget函数返回的消息队列标识码
cmd:是将要采取的动作,(有三个可取值),一般用IPC_RMID,这时候表示移除消息队列的意思
cmd的参数参考:
- IPC_STAT
读取消息队列的数据结构msqid_ds,并将其存储在buf指定的地址中。- IPC_SET
设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。- IPC_RMID
删除消息队列。
buf:一般写NULL
(5)键值生成ftok()函数
- 作用:系统IPC键值的格式转换函数,系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
通过ftok函数生成key值,函数ftok把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值(也称IPC key键值)。 - 函数原型:
//所需头文件
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id)
- 参数:
pathname:是你指定的文件名(已经存在的文件名),一般使用当前目录,如:
key_t key;
key = ftok(".", 1); //这样就是将pathname设为当前目录。可随便设置
proj_id:子序号。虽然是int类型,但是只使用8bits(1-255)。
消息队列示例
msgSnd.c 文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
key_t key;
int msgid;
int sendret;
int rcvret;
struct msgbuf sendMsg = {897,"hello thi is from que"};
struct msgbuf rcvBuf;
key = ftok(".", 1); //创建key值对
if(key == -1)
{
printf("faild to get the key\n");
}
printf("the key is:%x\n",key);
msgid = msgget(key,IPC_CREAT|0777);//创建消息队列,并赋予可读可写可执行权限
if(msgid < 0){
printf("creat que faild\n");
}
sendret = msgsnd(msgid,&sendMsg,strlen(sendMsg.mtext),0);//向消息队列发送消息
if(sendret < 0){
printf("send msg fail\n");
}else{
printf("send successfully the msg is %s \n",sendMsg.mtext);
}
rcvret = msgrcv(msgid,&rcvBuf,sizeof(rcvBuf.mtext),888,0);//接受队列中type为888的消息
if(rcvret < 0){
printf("recevive msg fail\n");
}else{
printf("get %d bytes : %s\n",rcvret,rcvBuf.mtext);
}
msgctl(msgid,IPC_RMID,NULL);//关闭消息队列
return 0;
}
msgRcv.c 文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
key_t key;
int msgid;
int sendret;
int rcvret;
struct msgbuf sendMsg = {888,"hello ,get over thank you"};
struct msgbuf rcvBuf;
key = ftok(".", 1); //创建key值对
if(key == -1)
{
printf("faild to get the key\n");
}
printf("the key is:%x\n",key);
msgid = msgget(key,IPC_CREAT|0777);//创建消息队列,并赋予可读可写可执行权限
if(msgid < 0){
printf("creat que faild\n");
}
rcvret = msgrcv(msgid,&rcvBuf,sizeof(rcvBuf.mtext),897,0);//接受队列中type为897的消息
if(rcvret < 0){
printf("recevive msg fail\n");
}else{
printf("get %d bytes : %s\n",rcvret,rcvBuf.mtext);
}
sendret = msgsnd(msgid,&sendMsg,strlen(sendMsg.mtext),0);//向消息队列发送消息
if(sendret < 0){
printf("send msg fail\n");
}else{
printf("send successfully ,Send msg is %s \n",sendMsg.mtext);
}
msgctl(msgid,IPC_RMID,NULL);//关闭消息队列
return 0;
}
运行结果
四、共享内存
1.共享内存相关概念
共享内存实际是操作系统在实际物理内存中开辟的一段内存。
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。
当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。
- 特点:
(1)共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
(2)因为多个进程可以同时操作,所以需要进行同步。
(3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问(共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。)
可以用指令 ipcs -m 查看存在的共享内存
可以用命令行来释放共享内存:ipcrm -m shmid(shmget返回值)
2.共享内存相关函数
//需要引入的头文件
#include <sys/ipc.h>
#include <sys/shm.h>
(1)shmget函数(共享内存创建)
- 作用:创建一个共享内存
- 函数原型:
int shmget(key_t key, size_t size, int flag);
-
参数:
key:为共享内存的名字,一般是ftok的返回值。
size:共享内存的大小,以page为单位,大小为4096的整数倍。 flag:权限标志,常用两个IPC_CREAT和IPC_EXCL,一般后面还加一个权限,相当于文件的权限。IPC_CREAT:创建一个共享内存返回,已存在打开返回 IPC_EXCL:配合着IPC_CREAT使用,共享内存已存在出错返回。 一般使用:IPC_CREAT | IPC_EXCL | 0666
-
返回值:
成功返回一个非负整数,即共享内存的标识码,失败返回-1。(key是内核级别的,供内核标识,shmget返回值是用户级别的,供用户使用的。)
2.shmat函数(挂载共享内存)
- 作用:使创建的共享内存与调用该函数进程的进程地址空间参数关联。
- 函数原型:
void *shmat(int shm_id, const void *addr, int flag);
- 参数:
shmid:共享内存的标识,shmget的返回值。
shmaddr:指定进程地址空间连接的地址。如果设置为null,默认让系统定要关联的地址。
shmflg: 权限,常见有两个SHM_RDONLY(只读)和SHM_REMAP(重新映射一个进程地址空间没这样shmaddr不能为空)。设为0,系统默认。
- 返回值:
返回映射到进程地址空间共享区的开始地址。此地址变为共享区的的地址,可通过此地址传输内容
3.shmdt函数(解挂共享内存)
- 作用:删除共享内存与进程地址空间的映射关系,将页表映射关系删除,释放进程地址空间。shmat和shmdt要一起使用才起作用。
- 函数原型:
int shmdt(void *addr);
- 参数:
shmaddr:共享内存映射到进程地址空间的地址。shmat返回值。 - 返回值:
成功返回0,失败返回-1
4.shmctl函数(控制共享内存)
- 作用:控制共享内存
- 函数原型:
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
- 参数:
shmid:共享内存的标识
cmd:以什么方式来控制共享内存。
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除这片共享内存,不要第四个参数,例: semctl(semid,0,IPC_RMID);
buf:指向一个共享内存的数据结构 。
共享内存管理结构体。具体说明参见共享内存内核结构定义部分
删除共享内存的时候,一般设置为NULL
- 返回值:成功返回0,失败返回-1。
共享内存示例
实现进程间通信步骤:
(1)创建共享内存
(2)共享内存关联进程
(3)删除共享内存与进程的关联
(4)释放共享内存
实现一个用户client与服务器server之间的简单通信。
需要client和server都和共享内存关联。client端不创建共享内存,不释放共享内存,server端创建共享内存,并且释放共享内存。
shmServer.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include <unistd.h>
int main()
{
key_t key;
int shmid;
key = ftok(".", 2); //创建key值对
if(key == -1)
{
perror("ftok error");
return 1;
}
printf("the key is:%x\n",key);
shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存
if(shmid == -1){
perror("shmget error");
return 1;
}
printf("shmid : %d\n",shmid);
char* addr = (char*)shmat(shmid,NULL,0);//挂载共享内存,返回的共享内存首地址保存在addr中
while(1){
printf("none of message\n");//读取共享内存的内容
sleep(1);
if(*addr != '\0'){
printf("message from sharememory :%s\n",addr);//读取共享内存的内容
break;
}
}
shmdt(addr);//删除关联
int sh = shmctl(shmid,IPC_RMID,NULL);//释放共享内存
if(sh == -1){
perror("shmctl");
return 1;
}
return 0;
}
shmCilent.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include <string.h>
int main()
{
key_t key;
int shmid;
char str[128] = {0};
key = ftok(".", 2); //创建key值对
if(key == -1)
{
perror("ftok error");
return 1;
}
printf("the key is:%x\n",key);
shmid = shmget(key,0,0);//创建共享内存,无需开辟空间,服务器读进程已经开辟空间
if(shmid == -1){
perror("shmget error");
return 1;
}
printf("shmid : %d\n",shmid);
char* addr = (char*)shmat(shmid,NULL,0);//挂载共享内存,返回的共享内存首地址保存在addr中
printf("输入向共享内存传递数据:\n");
scanf("%s",str);
strcpy(addr,str);
shmdt(addr);//删除关联
return 0;
}
五、信号
1.信号概念
(1)概念
对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。
(2)信号的名字和编号
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、 “SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
(3)信号处理
号的处理有三种方法,分别是:忽略、捕捉和默认动作
-
忽略信号:大多数信号可以被忽略,除了( SIGKILL和SIGSTOP)因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。
-
捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
-
系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。
信号处理函数的注册
信号处理函数的注册不只一种方法,分为入门版和高级版
入门版:函数signal
高级版:函数sigaction
信号处理发送函数
信号发送函数也不止一个,同样分为入门版和高级版
入门版:kill
高级版:sigqueue
2.信号编程(入门版)
(1) 注册和处理函数
signal 函数原型:
#include <signal.h>//需要引入的头文件
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。
对于sighandler_t signal(int signum, sighandler_t handler);
函数来说,signum 显然是信号的编号,handler 是中断函数的指针。
同样,typedef void (*sighandler_t)(int);中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。
#include <signal.h>
#include <stdio.h>
void handler(int signum)//中断函数
{
switch(signum){
case 2:
printf(" Get SIGINT\n");
break;
case 10:
printf(" Get SIGUSR\n");
break;
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
通过signal函数注册了一个信号处理函数,分别注册了三个信号,通过while循环维持这一进程。在信号处理函数捕捉这些信号,更改这些信号的默认处理方式。
比如我们直接在终端上输入Crt + C 即发送了SIGINT信号,按照SIGINT默认处理方式是结束这一进程,但它的默认处理已经被我们更改了,则进程不会退出。但值得注意的是,SIGKILL并不会被我们更改。
忽略信号:
在 man 手册中查看signal 的用法可以发现 信号的忽略
宏定义SIG_IGN 可以表示忽略此信号
#include <signal.h>
#include <stdio.h>
void handler(int signum)
{
switch(signum){
case 2:
printf(" Get SIGINT\n");
break;
case 10:
printf(" Get SIGUSR\n");
break;
}
}
int main()
{
signal(SIGINT,SIG_IGN);
signal(SIGUSR1,handler);
while(1);
return 0;
}
~
可以发现忽略了ctrl +c
(2)信号发送函数
kill函数原型:
#include <sys/types.h>//需要引入的头文件
#include <signal.h>
int kill(pid_t pid, int sig);
kill函数使用:
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
int main(int argc,char **argv)
{
int pid,signum;
pid = atoi(argv[2]);
signum = atoi(argv[1]);
kill(pid,signum);
return 0;
}
也可以用sprintf 搭配system 完成与kill一致的工作
int main(int argc,char **argv)
{
int pid;
int signum;
char cmd[128]={0};
pid=atoi(argv[2]);
signum=atoi(argv[1]);
sprintf(cmd,"kill -%d %d",signum,pid);//完成对cmd的格式设置,将cmd格式设置成 kill -x xxxx
printf("pid is %d signum is %d",pid,signum);
system(cmd);//用system直接调用命令来实现对信号的响应与控制
return 0;
}
运行./a.out程序
通过kill发送函数 发送SIGKILL信号停止该程序
3.信号编程——高级版(携带信息)
(1)注册和处理函数(高级)
sigaction 的函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};
//回调函数句柄sa_handler、sa_sigaction只能任选其一
说明:
- 函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中:
第一个参数signum:是注册的信号的编号
第二个参数act:如果不为空说明需要对该信号有新的配置;
第三个参数oldact:如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。
- 结构体 struct sigaction :
(1)void (*sa_handler)(int);
不携带数据,作用与入门版类似。
(2)void (*sa_sigaction)(int, siginfo_t *, void *);
处理函数来说还需要有一些说明。void*
是接收到信号所携带的额外数据;而struct siginfo
这个结构体主要适用于记录接收信号的一些相关信息。
注意:sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。
(3) sa_mask 成员:
sigset_t sa_mask
是一个信号集,在调用该信号捕捉函数之前,将需要block的信号加入这个sa_mask,仅当信号捕捉函数正在执行时,才阻塞sa_mask中的信号,当从信号捕捉函数返回时进程的信号屏蔽字复位为原先值;因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。
关于sa_mask的解释可以参考这篇文章sa_mask详解
(4) sa_flags: 是一个选项,注意:这个选项只与sigaction函数注册的信号有关联,与sa_mask中的信号无任何关系
SA_INTERRUPT 由此信号中断的系统调用不会自动重启
SA_RESTART 由此信号中断的系统调用会自动重启
SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针
SA_NODEFER 一般情况下,当信号处理函数运行时,内核将阻塞(sigaction函数注册时的信号)。但是如果设置了SA_NODEFER标记, 那么在该信号处理函 数运行时,内核将不会阻塞该信号。
SA_RESETHAND 当调用信号处理函数时或信号处理函数结束后,将信号的处理设置为系统默认值。
struct siginfo结构体说明
void (*sa_sigaction)(int signum,siginfo_t *info,void *ucontext);
void*
是接收到信号所携带的额外数据;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息。
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
int si_band; /* Band event */
int si_fd; /* File descriptor */
}
关于发送过来的数据是存在两个地方的,sigval_t si_value
这个成员中有保存了发送过来的信息;同时,在si_int
或者si_ptr
成员中也保存了对应的数据。
- sigaction使用 ——接收端
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
//void (*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum,siginfo_t *info,void*context)//信号处理函数
{
printf("get USR1 the signum is %d\n",signum);
if(context != NULL){
printf("from %d\n",info->si_pid);
printf("get data %d\n",info->si_int);
printf("get data %d\n",info->si_value.sival_int);
}
}
int main()
{
struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;//将标志位指定为此才可接收数据
sigemptyset(&act.sa_mask);//清空阻塞信号
sigaction(SIGUSR1,&act,NULL);
printf("My pid is %d\n",getpid());
while(1);
return 0;
}
(2) 信号发送函数(高级)
- sigqueue 函数原型
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
使用这个函数之前,必须要有几个操作需要完成
1.使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
2.sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。
sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。
sigqueue 使用(携带整型数字消息)——发送端
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
//int sigqueue(pid_t pid, int sig, const union sigval value);
int main(int argc,char **argv)
{
int pid;
int signum;
signum = atoi(argv[1]);
pid = atoi(argv[2]);
union sigval value;
value.sival_int = 66;
sigqueue(pid,signum,value);
return 0;
}
~
运行效果:
接收端:
发送端:
六、信号量
1.信号量相关概念
与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
信号量的操作:信号量是一种特殊的变量,对信号量的访问必须是原子操作,信号量的操作只有两种:P操作(-1,申请资源)和V操作(+1,释放资源)。最简单的信号量只有两种取值0和1,称这样的信号量为二元信号量。可以取值为正整数N的信号量称为多元信号量,它允许多个线程并发的访问资源。
- 信号量相关定义:
(1)临界资源:能被多个进程共享,但一次只能允许一个进程使用的资源称为临界资源。
(2)临界区:涉及到临界资源的部分代码,称为临界区。
(3)互斥:亦称间接制约关系,在一个进程的访问周期内,另一个进程就不能进行访问,必须进行等待。当占用临界资源的进程退出临界区后,另一个进程才允许去访问此临界资源。
例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。
(4)同步:亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的同步就是源于它们之间的相互合作。所谓同步其实就是两个进程间的制约关系。
例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。
(5)原子性:对于进程的访问,只有两种状态,要么访问完了,要么不访问。当一个进程在访问某种资源的时候,即便该进程切出去,另一个进程也不能进行访问。
- 信号量特点:
(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
(2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
(3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
(4)支持信号量组
2.信号量集函数
(1)semget函数(创建和访问一个信号量组)
- 作用:用来创建和访问一个信号量组。
- 函数原型:
int semget(key_t key, int nsems, int semflg);
-
参数:
key:建值,信号量集的名字(通过ftok获取)
nsems:信号量集中信号量的个数
semflg:标识函数的行为及权限。取值如下:IPC_CREAT:如果不存在就创建 IPC_EXCL和IPC_CREAT搭配使用,如果已经存在,则返回失败
-
返回值:成功返回一个非负整数,即信号量集的标识码,失败返回-1
-
运用示例:
semid = semget(key,1,IPC_CREAT|0666);//获取或创建信号量
(2)semop函数(操作信号量组)
- 作用:对信号量组进行操作,改变信号量的值
- 函数原型:
int semop(int semid, struct sembuf *sops, unsigned nsops);
- 参数:
semid:由semget返回的信号量集的标识码
sops:指向一个信号量结构体的指针
struct sembuf{
unsigned short sem_num; 信号量的编号
short sem_op; 信号量一次PV操作时加减的数值,一般只会用到两个值:
-1:P操作,等待信号变得可用;+1:V操作,发出信号量已经变得可用
short sem_flg; 标志,有两个取值:
IPC_NOWAIT:表示队列满不等待,非阻塞,返回EAGAIN错误。
IPC_UNDO: 使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
};
nsops:信号量的个数
- 返回值:成功返回0,失败返回-1
- 运用示例:
struct sembuf init;
init.sem_num = 0;
init.sem_op = -1;
init.sem_flg = 0;
semop(semid,&init,1);
(3)semctl函数(控制信号量)
- 作用:用于控制信号量集
- 函数原型:
int semctl(int semid, int semnum, int cmd, ...);
-
参数详解:
semid:由semget返回的信号量集的标识码
semnum:信号量集中信号量的序号
cmd: 将要采取的动作,取值如下:SETVAL:设置信号量集中信号量的计数值 GETVAL:获取信号量集中信号量的计数值 IPC_STAT:把semid_ds结构中的数据设置为信号量集的当前关联值 IPC_SET:在进程有足够权限的前提下,把信号集的当前关联值设置为semid_ds数据结构中给出的值 IPC_RMID:删除信号量集,不要第四个参数
最后一个参数根据命令的不同而不同:
当第三个参数为SETVAL时即初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下:
// 用于信号等操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
- 返回值:成功返回0,失败返回-1
- 运用示例:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
union semun set;
set.val = 1; //信号量集中的第几个信号
semctl(semid,0,SETVAL,set);//初始化信号量
//SETVAL设置信号量的值,这里设置为set
信号量控制进程示例
- 父子进程执行顺序问题
保证父进程在子进程执行完毕后执行
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//p操作
void pGetKey(int semid)
{
struct sembuf init;
init.sem_num = 0;
init.sem_op = -1;
init.sem_flg = SEM_UNDO;
semop(semid,&init,1);
printf("pGetKey ok!\n");
}
//v操作
void vPutBackKey(int semid)
{
struct sembuf init;
init.sem_num = 0;
init.sem_op = 1;
init.sem_flg = SEM_UNDO;
semop(semid,&init,1);
printf("vPutBackKey ok!\n");
}
// 用于信号等操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int main()
{
key_t key;
int semid;
pid_t pid;
key = ftok(".",111);//创建键值对
semid = semget(key,1,IPC_CREAT|0666);//创建信号量集,此信号量集只有一个信号量
if(semid < 0){
perror("creat semid error");
return 1;
}
union semun seminit;
seminit.val = 0;
int ctlRet = semctl(semid,0,SETVAL,seminit);//设置信号量集中信号量的计数值
if (ctlRet< 0){
perror("semctl error");
return 1;
}
pid = fork();//创建子进程,用pv操作,使得子进程执行完毕后在执行父进程
if(pid > 0){
pGetKey(semid);//先去拿锁,若子进程已经执行完,则存在锁,可执行
printf("this is father the pid is %d\n",getpid());
vPutBackKey(semid);//执行完后放回锁
semctl(semid,0,IPC_RMID);//删除信号量集
}else if(pid == 0){
printf("this is child the pid is %d\n",getpid());
vPutBackKey(semid);//子进程执行完后,创建锁
}
return 0;
}
- 与共享内存一起使用,进行读写的限制
下例,只有客户端写完数据到共享内存中,服务器端才可从内存中读取
shmsemCilent.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include <string.h>
#include <sys/sem.h>
#include <unistd.h>
//p操作
void pGetKey(int semid)
{
struct sembuf init;
init.sem_num = 0;
init.sem_op = -1;
init.sem_flg = SEM_UNDO;
semop(semid,&init,1);
printf("pGetKey ok!\n");
}
//v操作
void vPutBackKey(int semid)
{
struct sembuf init;
init.sem_num = 0;
init.sem_op = 1;
init.sem_flg = SEM_UNDO;
semop(semid,&init,1);
printf("vPutBackKey ok!\n");
}
// 用于信号等操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int main()
{
key_t shmkey;
key_t semkey;
int shmid;
int semid;
char str[128] = {0};
shmkey = ftok(".", 2); //创建共享内存key值对
if(shmkey == -1)
{
perror("ftok shmkey error");
return 1;
}
semkey = ftok(".",4);//创建信号量key值对
if(semkey == -1)
{
perror("ftok semkey error");
return 1;
}
printf("共享内存的key值:%x\n",shmkey);
printf("信号量的key值:%x\n",semkey);
shmid = shmget(shmkey,0,0);//创建共享内存,无需开辟空间,服务器读进程已经开辟空间
if(shmid == -1){
perror("shmget error");
return 1;
}
semid = semget(semkey,1,IPC_CREAT|0666);//创建信号量集,此信号量集只有一个信号量
if(semid < 0){
perror("creat semid error");
return 1;
}
union semun seminit;
seminit.val = 0;
int ctlRet = semctl(semid,0,SETVAL,seminit);//设置信号量集中信号量的计数值
if (ctlRet< 0){
perror("semctl error");
return 1;
}
char* addr = (char*)shmat(shmid,NULL,0);//挂载共享内存,返回的共享内存首地址保存在addr中
printf("输入向共享内存传递数据:\n");
scanf("%s",str);
strcpy(addr,str);
vPutBackKey(semid);//客户端向共享内存写完内容后,创建锁
shmdt(addr);//删除关联
return 0;
}
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include <unistd.h>
#include <sys/sem.h>
#include <unistd.h>
//p操作
void pGetKey(int semid)
{
struct sembuf init;
init.sem_num = 0;
init.sem_op = -1;
init.sem_flg = SEM_UNDO;
semop(semid,&init,1);
printf("pGetKey ok!\n");
}
//v操作
void vPutBackKey(int semid)
{
struct sembuf init;
init.sem_num = 0;
init.sem_op = 1;
init.sem_flg = SEM_UNDO;
semop(semid,&init,1);
printf("vPutBackKey ok!\n");
}
// 用于信号等操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int main()
{
key_t shmkey;
key_t semkey;
int shmid;
int semid;
char* addr;//共享内存地址
shmkey = ftok(".", 2); //创建共享内存key值对
if(shmkey == -1)
{
perror("ftok shmkey error");
return 1;
}
semkey = ftok(".",4);//创建信号量key值对
if(semkey == -1)
{
perror("ftok semkey error");
return 1;
}
printf("共享内存的key值:%x\n",shmkey);
printf("信号量的key值:%x\n",semkey);
shmid = shmget(shmkey,4096,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存
if(shmid == -1){
perror("shmget error");
return 1;
}
semid = semget(semkey,1,IPC_CREAT|0666);//创建信号量集,此信号量集只有一个信号量
if(semid < 0){
perror("creat semid error");
return 1;
}
union semun seminit;
seminit.val = 0;
int ctlRet = semctl(semid,0,SETVAL,seminit);//设置信号量集中信号量的计数值
if (ctlRet< 0){
perror("semctl error");
return 1;
}
pGetKey(semid);//当客户端向共享内存完写数据时,在读写。
addr = (char*)shmat(shmid,NULL,0);//挂载共享内存,返回的共享内存首地址保存在addr中
vPutBackKey(semid);//解锁
printf("message from sharememory :%s\n",addr);//读取共享内存的内容
semctl(semid,0,IPC_RMID);//删除信号量集
shmdt(addr);//删除关联
int sh = shmctl(shmid,IPC_RMID,NULL);//释放共享内存
if(sh == -1){
perror("shmctl");
return 1;
}
return 0;
}
- 运行结果