linux操作系统下进程间的通信总结
一. 进程间通信
1.1 进程间通信的概述
(1)进程用户空间是相互独立的,一般而言是不能相互访问的;但很多情况下进程间需要互相通信,来完成系统的某项功能;进程通过与内核及其它进程之间的互相通信来协调它们的行为。
(2)进程之间交换信息的方法只能是经由fork或exec传送文件打开文件,或者通过文件系统。
(3)IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
2.2 进程通信的应用场景
(1)数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
(2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
(3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
(4)资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
(5)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二.管道通信
2.1 管道
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
2.1.1 管道特点
(1)半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
(2)只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
(2)可以看成是一种特殊的文件,对于读写也可以使用普通的read、write 等函数;但它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
2.1.2 函数原型
#include <unistd.h>
int pipe(int fd[2]);
// 返回值:若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
2.2 管道编程实例
(1)单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道执行完读写操作。
(2)若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。
//demo1.c
#include <stdio.h>
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
int fd[2];//两个文件描述符
int pid;//pid函数就和创建子进程道理一样,创建一个进程
char buf[128];//开辟一个128字符空间
// int pipe2(int pipefd[2], int flags);
// pid_t fork(void);
if(pipe(fd) == -1){ //如果创建失败的话
printf("creat pipe failed\n"); //创建管道失败
}
pid = fork(); //调用fork函数创建新进程
if(pid < 0){ //pid小与0的时候的管道创建成功
printf("creat child failed\n");
}
else if(pid > 0){ //当pid大于0则是父进程
sleep(4); //睡眠4秒
printf("this is father\n");
close(fd[0]);//关闭读段
write(fd[1], "hello from father", strlen("hello from father")); //写段写的数据写入数据缓冲区中,长度为写入数据的长度。
wait;
}else{
printf("this is child\n");
close(fd[1]);//关闭写端
read(fd[0], buf, 128); //read函数的具体操作,开辟128个字符空间。
printf("read from father :%s\n",buf); //打印信息
exit(0);//退出
}
return 0;
}
2.2.1 运行效果图
2.3 创建命名管道(FIFO)
FIFO,也称为命名管道,它是一种文件类型。
2.3.1 特征
1.FIFO可以在无关的进程之间交换数据,与无名管道不同。
2.FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
2.3.2 函数原型
#include <sys/stat.h>
// 返回值:成功返回0,出错返回-1
int mkfifo(const char *pathname, mode_t mode);
1.参数解释
1.其中的 mode 参数与open函数中的 mode 相同。
2.pathname这代表处理文件的绝对路径以及可执行项目文件。
3.一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
解释一下mode哪里为什么写EEXIST
2.3.3 操作实例
通过创建一个FIFO并且命名为fifo,这个管道有执行权限,并且成功创建和失败后的返回值不同,而这段代码有if循环替代返回值,把输出结果打印出来,是的可读性更高。
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
//demo4.c
// int mkfifo(const char *pathname, mode_t mode);
int main()
{
if( ( mkfifo("./fifo",0600) == -1) && errno == EEXIST){
printf("mkfifo failed\n");
perror("why");
}
else{
if(errno == EEXIST){
printf("fife exist");
}else
printf("mkfifo successed\n");
}
return 0;
}
2.3.5 运行效果图
管道成功创建和失败创建两种不同的结果
2.4 命名管道的数据通信编程实现
如果管道创建以后就用open函数打开,在管道里面实现读写的操作,相当于平日接收消息。
2.4.1 名管道的数据通信的注意事项
当 open 一个FIFO时,是否设置非阻塞标(O_NONBLOCK)的区别:
1.若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
2.若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
2.4.2 实例操作
- 在同一个管道里面实现数据的写入和读取操作,第一段代码执行写入操作,第二段代码执行读取操作。
#include <stdio.h>
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
//write.c
// int mkfifo(const char *pathname, mode_t mode);
int main()
{
int cnt = 0;
char *str = "message from fifo";
int fd = open("./fifo",O_WRONLY);
printf("write open success\n");
while(1){
write(fd, str, strlen(str));
sleep(2);
if(cnt){
break;
}
}
close(fd);
return 0;
}
#include <stdio.h>
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
//read.c
// int mkfifo(const char *pathname, mode_t mode);
int main()
{
char buf[30] = {0};
int nread = 0;
if( ( mkfifo("./fifo",0600) == -1) && errno!= EEXIST){
printf("mkfifo failed\n");
perror("why");
}
int fd = open("./fifo",O_RDONLY);
printf("open success\n");
while(1){
nread = read(fd,buf,30);
printf("read %d byte from fifo,context:%s\n",nread,buf);
}
close(fd);
return 0;
}
2.4.3 运行效果图
一个在管道里面写入成功后另一方就在不断地读取数据
三.消息队列的通信原理
3.1 什么是消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
3.1.2 消息队列的特点
1.消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2.消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
3.2 消息队列相关的API(msgget、msgrcv、msgsnd)
1.msgget
#include <sys/msg.h>
创建或打开消息队列:成功返回队列ID,失败返回-1
2.msgsnd
添加消息:成功返回0,失败返回-1
3.msgrcv
读取消息:成功返回消息数据的长度,失败返回-1
4.msgctl
控制消息队列:成功返回0,失败返回-1
3.2.1 函数原型
int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);*/
3.2.2操作API的注意事项
1. 在以下两种情况下,msgget将创建一个新的消息队列
1.如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
2.key参数为IPC_PRIVATE。
3. msgget()函数的第一个参数是消息队列对象的关键字(key),函数将它与已有的消息列对象的关键字进行比较来判断消息队列对象是否已经创建。
4.而函数进行的具体操作是由第二个参数,msgflg 控制的。它可以取下面的几个值:
(1)IPC_CREAT :
如果消息队列对象不存在,则创建之,否则则进行打开操作;
(2)IPC_EXCL:
和IPC_CREAT 一起使用(用”|”连接),如果消息对象不存在则创建之,否则产生一个错误并返回。
(3)如果单独使用IPC_CREAT 标志,msgget()函数要么返回一个已经存在的消息队列对象的标识符,要么返回一个新建立的消息队列对象的标识符。如果将IPC_CREAT 和IPC_EXCL标志一起使用,msgget()将返回一个新建的消息对象的标识符,或者返回-1 如果消息队列对象已存在。
(4)IPC_EXCL 标志本身并没有太大的意义,但和IPC_CREAT 标志一起使用可以用来保证所得的消息队列对象是新创建的而不是打开的已有的对象。
3.4 消息队列编程收发数据操作实例
- 这两段代码第一段相当于发送端,第两端段代码相当于获取段,一个执行发送命令一个执行获取命令。
- 分析第一段代码首先要看查看man手册看一下msgget、msgsnd、msgrcv这三个函数的原型,如何使用,以及里面的参数代表着什么。
- 注意参数KEY如何去定义.
- 尤其解释一下代码出现了888和988是属于消息的类型,就是msgrcv函数里面的这个参数long msgtyp。
- sendBuf.mtext这个参数则代表的一个数组的大小观察原函数就可明白。
- msgflg需要什么类型的在man手册查询以后看属于那种情况进行使用。
- 代码大体分为三个步奏创建、发送、接收、控制;接收端代码在发送和控制的顺序不同而已;其中细节部分则是获取key值,打开消息队列。
- 最后的发送方式务必要注意一下,以非阻塞的方式进行发送。
3.5 键值生成及消息队列移除
3.5.1 ftok的使用
- 系统IPC键值的格式转换函数
- 系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
- 函数原型:
key_t ftok( const char * fname, int id )
- key = ftok(".", z); 这样就是将fname设为当前目录。
**id是子序号。虽然是int类型,**但是只使用8bits(1-255)。
3.5.2 msgrcv 函数
- 函数msgrcv在读取消息队列时,type参数有下面几种情况**
1.type == 0,返回队列中的第一个消息;
2.type > 0,返回队列中消息类型为 type 的第一个消息;
3.type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
2.可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。
3.5.3 案例分析
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//msgSend.c
/* int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);*/
struct msgbuf{
long mtype; //message type, must be > 0*/
char mtext[128]; //message data */
};
int main()
{
//1.huoqu
struct msgbuf readBuf;
struct msgbuf sendBuf = {888,"this is message from quen"};
key_t key;\\打开KEY值
key =ftok(".",'z');\\获取key值
printf("key=%x\n",key);
int msgId = msgget(key, IPC_CREAT|0777); \\打开消息队列
if(msgId == -1){
printf("get que failed\n");
}
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);\\发送消息;最后的0代表以非阻塞的方式发送
printf("send over\n");
msgrcv(msgId, &readBuf, sizeof(readBuf.mtext),988,0); \\接受类型为988的消息
printf("read from que:%s\n",readBuf.mtext);
msgctl(msgId,IPC_RMID,NULL); \\将该队列从内核中删除
return 0;
}
include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//msgGet.c
/* int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);*/
struct msgbuf{
long mtype;
char mtext[128];
};
int main()
{
//1.huoqu
struct msgbuf readBuf;
key_t key;\\获取KEY值
key =ftok(".",'z');
printf("key=%x\n",key);
int msgId = msgget(key, IPC_CREAT|0777);\\创建消息队列
if(msgId == -1){
printf("get que failed\n");
}
msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);\\接受一个类型为888的消息队列
printf("read form que:%s\n",readBuf.mtext);
struct msgbuf sendBuf = {988, "thank you for reach\n"};\\发送消息到988类型的消息中
msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext),0); \\其中最后的0代表以非阻塞的方式发送
msgctl(msgId,IPC_RMID,NULL);\\从内核中删除该消息队列
return 0;
}
3.5.2 运行效果图
四.共享内存
4.1 共享内存原理概述
- 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
4.1.2 特点
- 共享内存是最快的一种 IPC,进程是直接对内存进行存取。
- 多个进程可以同时操作,所以需要进行同步。
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
4.2 shmget 、shmat 、shmdt、 shmctl函数解释
-
#include <sys/shm.h>(头文件)
int shmget(key_t key, size_t size, int flag);
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1 -
void *shmat(int shm_id, const void *addr, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1 -
int shmdt(void *addr);
// 断开与共享内存的连接:成功返回0,失败返回-1 -
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
// 控制共享内存的相关信息:成功返回0,失败返回-1
4.3 共享内存编程实例
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
//shmw.c
//int shmget(key_t key, size_t size, int shmflg);
int main()
{
int shmid; \\信号量的标志
char *shmaddr;
key_t key;
key = ftok(".",1);
shmid = shmget(key,1024*4,IPC_CREAT|0666); \\开辟的空间必须是1024个字节
if(shmid == -1){ \\判断共享内存是否创建成功
printf("shmget noOK\n");
exit(-1);
}
shmaddr = shmat(shmid,0,0);
printf("shmat ok\n");
strcpy(shmaddr,"xutanghao\n"); //需要拷贝的东西
sleep(5);
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, 0);
printf("quit\n");
return 0;
}
4.3.1 运行效果图
五.信号
5.1 信号的概念
- 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
- 信号定义在signal.h头文件中,信号名都定义为正整数。
- 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
- 如何查看系统里面的信号—kill-l
5.1.1 如何使用信号
- 对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。
- 如,我在后台运行了一个 top 工具,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。
- 如果查看信号编号和名称,可以发现9对应的是 9–SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。
5.1.2 信号处理
- 信号的处理有三种方法:
忽略、捕捉和默认动作
5.2 信号的编程实例(信号处理)
- 两端代码相当于一种应答机制,让第二段代码相当于kill指令的存在,第一段代码存在三种应答机制
- 如果要把第一段代码执行kill指令则需要在grep查找它的端口号进行查询。
5.2.1 atoi函数
- C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。
- 函数原型:
int atoi(const char *str);
- 头文件:
#include <stdlib.h>
- 一句话总结一下就是字符串转成int型。
//singaldemo1.c
#include <signal.h>
#include <stdio.h>
//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);
void handler(int signum)//定义三个信号类型,并且
{
printf("get signum=%d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
printf("never quit\n");
}
int main()
{
signal(SIGINT,handler);
signal(SIGKILL,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
//signaldemo1CON.c
//int kill(pid_t pid, int sig);
//Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
//kill(): _POSIX_C_SOURCE
int main(int argc ,char **argv)//在运行的时候需要输入两个参数
{
int signum;
int pid;
char cmd[128]={0};
signum = atoi(argv[1]);
pid = atoi(argv[2]);
printf("num=%d,pid=%d\n",signum,pid);
//kill(pid, signum);
sprintf(cmd, "kill -%d %d\n",signum,pid);
system(cmd);
printf("send signal ok");
printf("line:%d\n",__LINE__);
return 0;
}
5.3 信号如何携带消息
1.明确两个概念如何发信号和接收信号。
2.发信号就分为一下两个步骤
(1)如何发信号。
(2)怎末放入消息。
3.收信号也是同样两个步骤
(1)用什么哪几种函数进行操作。
(2)如何读取到消息。
5.3.1 sigqueue原型函数(信号发送函数)
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
5.3.2 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只能任选其一
5.3.3 使用参数的注意事项
-
void (*sa_sigaction)(int, siginfo_t *, void *);
处理函数来说还需要有一些说明。 -
void*
是接收到信号所携带的额外数据;而struct siginfo
这个结构体主要适用于记录接收信号的一些相关信息。 -
sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum应该就是注册的信号的编号;第二个参数act如果不为空说明需要对该信号有新的配置;第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。
5.3.4 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 */
}
5.4 信号携带消息编程实例
5.4.1 代码的解析(接收信号端的代码)
1.首先使用sigaction函数进行注册信号,然后观察里面的三个参数,第一个参数是接受哪一种参数,第二个参数是接收到信号以后做什么事情,最后一个参数主要是为了备份。
2.第二个参数act,如果让能够接收到信号就必须flag等于SA_SIGINFO。
3.在接受到信号以后由 handler进行处理信号。
4.在handler接收到信号以后把信号的值打印出来,紧接着看handler第二个参数是把信号里面内容打印出来,内容的有无取决与最后一个参数context。
5.context参数如果是非空状态则有内容,反之则无;打印的内容则是在info地址下的si_int数据。
#include <signal.h>
#include <stdio.h>
//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
//int sigqueue(pid_t pid, int sig, const union sigval value);
//Nicesignal.c
void handler(int signum, siginfo_t*info, void *context)
{
printf("get signum %d\n",signum);
if(context != NULL){
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;
sigaction(SIGUSR1,&act,NULL);
while(1);
return 0;
}
5.4.2 发送端代码解析
- 首先一定要清楚sigqueu函数里面的三个参数的使用方法。
- 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
- sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。
- sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。
- union sigval value这个参数是一个联合体需要去定义一下,最后给value的宏sival_int配置为100即可。
- 注意定义的pid号和信号数。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
//int sigqueue(pid_t pid, int sig, const union sigval value);
//send.c
int main(int argc, char **argv)
{
int signum; //发送信号
int pid; //pid号
signum = atoi(argv[1]);
pid = atoi(argv[2]);
union sigval value;
value.sival_int = 100;
sigqueue(pid,signum,value);
printf("%d,done\n",getpid());
return 0;
}
5.5 运行效果图
1.记住运行send函数需要两个参数,不然就会报错。
六. 信号量概述
- 信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
6.1特点
(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
(2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
(3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
(4)支持信号量组。
6.2 临界资源
- 多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。
- 一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
6.3 信号量编程实例
- 通过运行结果我们可以观察出来,首先是子进程先运行,是因为父进程在创建以后,被阻塞没有拿到锁,而子进程在运行程序以后让父进程拿到锁才能执行下面的代码。
- 注意p操作和v操作的实际含义(务必看注释)。
- 还有就是父子进程的拿锁操作,如果没有这步操作,就无法进程下面的操作。
- 如果哪一个函数没有看明白,看一下原函数或者查一下man手册。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//key_t ftok(const char *pathname, int proj_id);
//int semget(key_t key, int nsems, int semflg);
//int semop(int semid, struct sembuf *sops, size_t nsops);
// int semctl(int semid, int semnum, int cmd, ...);
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
void pGetKey(int id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = -1;
set.sem_flg=SEM_UNDO;
semop(id, &set, 1);
printf("getkey\n");
}
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
void vPutBackKey(int id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg=SEM_UNDO;
semop(id, &set, 1);
printf("put back the key\n");
}
int main(int argc, char const *argv[])
{
key_t key;
int semid;
key = ftok(".",2);//信号量集合中有一个信号量
semid = semget(key , 1, IPC_CREAT|0666);//获取/创建信号量
union semun initsem;
initsem.val = 0;
//操作第0个信号量
semctl(semid, 0, SETVAL, initsem);//初始化信号量
//SETVAL设置信号量的值,设置为inisem
int pid = fork();
if(pid > 0){
//去拿锁
pGetKey(semid);
printf("this is father\n");
vPutBackKey(semid);
//锁放回去
semctl(semid,0,IPC_RMID);
}
else if(pid == 0){
printf("this is child\n");
vPutBackKey(semid);
}else{
printf("fork error\n");
}
return 0;
}
6.3.1 运行效果图
七. 五种通信方式的总结
1. 管道:速度慢,容量有限,只有父子进程能通讯
2. FIFO:任何进程间都能通讯,但速度慢
3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
4. 信号量:不能传递复杂消息,只能用来同步
5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存