概念
调用fork()之后创建子进程,子进程之间相互独立,每个进程都有自己的用户地址
空间,也就说进程之间的变量不共享,如果多个进程需要相互协作完成一项任务
时,必然会进行进程的同步和数据的交换,所以实现进程之间数据的交换必须通
过内核,在内核中开辟出一块缓冲区。一个进程把自己的数据从用户空间拷贝到
内核缓冲区,另一个进程再从内核缓冲区把数据读走。内核提供的这种机制称为
进程间通信机制(IPC)。
进程间通信的7种方式
1、信号
Linux下信号机制类似于单片机中的中断机制,信号是对硬件层中断的一种异步通信机制的模拟,无需CPU轮询等待。linux下kill命令可以查看系统所支持的信号。
-
Linux下信号机制模型
对上图的解释:
每个进程都会维护着一个进程控制块(PCB),在这个PCB中保留着信号位图信息,当进程调用signal()、sigaction()安装信号或者其他进程给本进程发送信号的时候,PCB中的信号位图对应的信号标志位会被置1,表示内核正在监测这个信号。当硬件中断或者某种信号被触发时,内核会暂停当前正在运行的进程,并将此时程序的位置(中断点)压入栈保存起来,进程从用户态切换为内核态并检查信号位图究竟是哪个信号被触发,然后返回用户态开始执行信号处理函数,信号处理结束后,中断点出栈,程序从该位置继续运行。
当然内核态的进程对信号还有两种处理方式,比如选择忽略该信号、执行默认动作。
-
一个父子进程通过信号通信的例子
程序思路:父子进程的运行分别由运行标志变量控制,父子进程通过kill函数发信号改变运行标志变量的值。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
//定义全局变量
int child_run = 0;//子进程运行标志默认0
int parent_run = 0;//父进程运行标志默认0
void sig_child(int signum)
{
if(SIGUSR1 == signum)
{
child_run = 1;//子线程运行标志置1
}
}
void sig_parent(int signum)
{
if(SIGUSR2 == signum)
{
parent_run = 1;//父进程运行标志置1
}
}
int main(int argc, char **argv)
{
pid_t pid = -1;//定义进程id
signal(SIGUSR1,sig_child);//注册信号和指定信号处理程序
signal(SIGUSR2,sig_parent);
pid = fork();//创建子进程
if(pid < 0)
{
printf("进程创建失败\n");
exit(0);
}
else if(pid == 0)//子进程
{
printf("子进程创建成功,等待父进程发运行信号\n");
while(!child_run)//子进程在此等信号
{
}
printf("信号到达,子进程开始运行\n");
kill(getppid(),SIGUSR2);//子进程给父进程发运行信号
return 0;//子进程退出
}
else
{
printf("父进程等待子进程发运行信号\n");
kill(pid,SIGUSR1);//给子进程发运行信号
while(!parent_run)//父进程在此等待运行信号
{
}
printf("父进程开始运行\n");
wait(NULL);//父进程等待子进程退出,避免僵死进程
}
return 0;
}
首先先将程序中kill(pid,SIGUSR1)注释掉,父子进程分别阻塞…
再将注释取消,父子进程正常运行…
- 信号只能被当前运行的进程所接收,信号发送但没有被接收称为挂起信号。
- 进程可以发送多个信号给一个进程,只不过内核帮接收信号的进程做信号排队处理。
- 以下给出几个关于信号的系统调用
kill() 向进程发送一个信号
sigaction() 注册信号
signal() 也是注册信号,两者差别后面讲
sigpending() 检查是否有挂起的信号
sigprocmask() 修改阻塞信号屏蔽码
sigsuspend() 等待一个信号
- signal()和sigaction
1、signal()
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数相对简单,没有阻塞信号的功能,当多个信号发送过来时,进程来不及处理,只能丢弃。
2、sigaction()
stuct sigaction
{
void (*)(int) sa_handle; //信号处理函数
sigset_t sa_mask; //信号屏蔽集
int sa_flags; //SA_NODEFER,如果设置来该标志,则不进行当前处理信
//号到阻塞;SA_RESETHAND,如果设置来该标志,则处理完
//当前信号后,将信号处理函数设置为SIG_DFL行为
}
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
这个函数会对多个信号做排队处理。
-
信号分类
1、规则信号(regular signal,编号1-31),也称为不可靠信号:无论发送多少次,在接收进程处理之前,重复的信号会被合并为一个,信号可能会丢失。2、实时信号(real-time signal,编号32-63),也称为可靠信号:发送多次,就会在接收进程的信号队列中出现多少次。
2、管道
管道实质是一段内核缓冲区,这一段缓冲区类似队列,进程写入的数据由内核定向给另一进程,另一进程读出数据,这种数据流向也称单工。此管道只能在具有亲缘关系的进程才能通信,比如父子进程。
图解:
-
pipe()系统调用在内核空间创建管道,并返回两个文件描述符fd[0]、fd[1],fd[0]是只读描述符,fd[1]是只写描述符。
-
fork()系统调用创建子进程,子进程继承父进程中的文件描述符,所以父子进程用户空间中都存在对管道操作的两个文件描述符,如上图所示。那么父子进程都有各自的写端和读端,父进程将如何实现数据的交流呢。
-
很简单,比如父进程向子进程发送数据,我们只需将父进程的读端fd[0]关闭,将子进程写端fd[1]关闭,这样就可以实现数据的交流。如下图所示:
-
就上面的陈述,将其用代码实现如下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int fd[2];//管道描述符数组
int rv = -1;//返回值
pid_t pid = -1;//进程id
char buff[30];
memset(buff,0,sizeof(buff));
if(pipe(fd) < 0)//创建管道
{
printf("创建管道失败:%s\n",strerror(errno));
}
pid = fork();//创建进程
if(pid < 0)
{
printf("创建进程失败:%s\n",strerror(errno));
return 0;
}
else if(pid == 0)
{
printf("子进程开始运行并开始从管道读数据...\n");
close(fd[1]);//子进程关闭写端
rv = read(fd[0],buff,sizeof(buff));//开始读数据
if(rv < 0)
{
printf("子进程读数据失败:%s\n",strerror(errno));
return -1;
}
else
{
printf("子进程读到[%d]字节:%s\n",rv,buff);
}
close(fd[0]);//子线程关闭读端
printf("子进程已退出\n");
return 0;
}
printf("父进程开始运行并开始向子进程写数据...\n");
close(fd[0]);//父进程关闭读端
rv = write(fd[1],"hi,my child,I am your father!",30);//父进程向子进程写数据
if(rv < 0)
{
printf("父进程写数据失败:%s\n",strerror(errno));
}
printf("父进程正在等待子进程退出...\n");
wait(NULL);
close(fd[1]);//父进程关闭写端
printf("父进程退出!\n");
return 0;
}
运行上面代码
3、命名管道(FIFO)
FIFO相对于管道,其最大的优点是任意进程之间可以相互通信,而不仅仅局限于亲缘进程,创建FIFO时,像无名管道一样在内核中创建了一个缓冲区,除此之外,FIFO在用户空间层以文件的形式存在,它有文件该有的属性,比如i节点,但是它没有数据块,FIFO的文件名相当于一个链接,其真正的数据存储在内核缓冲区中,正因为FIFO以文件的形式存在,多个任意进程可以访问。有因为没有数据块的原因,FIFO的访问速度比访问一般文件要快很多。
-
创建并打开一个FIFO
进程通过mknod()或者mkfifo系统调用创建一个FIFO,FIFO创建之后可以使用一般IO系统调用进行访问,比如open、read、write、close等,顶层操作与一般文件类似,对于内核,由于FIFO是一类特殊的文件,内核操作还是有区别的。 -
一个例子
应用多路复用epoll管理文件描述符,从而实现两个进程之间对话:
程序实现上图所示的一个两个进程之间全双工通信机制:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/wait.h>
int g_stop = 0;
//管道破裂处理程序
void sig_pipe(int signum)
{
if(SIGPIPE == signum)
{
printf("get pipe broken signal and let programe exit\n");
g_stop = 1;
}
}
int main(int argc, char **argv)
{
int fdr_fifo;
int fdw_fifo;
char buf[1024];
struct epoll_event ctl_event;
struct epoll_event rv_event[2];
int rv = -1;
int rvctl = -1;
int rvwait = -1;
int epollfd;
int i;
//判断文件是否存在
if(access(".fifo1",F_OK) < 0)
{
printf(".fifo1不存在,将要被创建!\n");
mkfifo(".fifo1",0777);
}
if(access(".fifo2",F_OK) < 0)
{
printf(".fifo2不存在,将要被创建!\n");
mkfifo(".fifo2",0777);
}
signal(SIGPIPE, sig_pipe);
//打开管道,通过终端命令行控制应该打开哪一个管道
if(0 == atoi(argv[1]))
{
fdr_fifo = open(".fifo1",O_RDONLY);//另外一个进程要首先以写模式打开FIFO1,否则两个进程都会被阻塞
if(fdr_fifo < 0)
{
printf("fifo1读模式打开失败:%s\n",strerror(errno));
exit(0);//程序退出
}
printf("成功打开fifo1的读模式!\n");
fdw_fifo = open(".fifo2",O_WRONLY);
if(fdw_fifo < 0)
{
printf("fifo2写模式打开失败:%s\n",strerror(errno));
exit(0);//程序退出
}
printf("成功打开fifo2的写模式!\n");
}
else
{
fdw_fifo = open(".fifo1",O_WRONLY);//另外一个进程要首先以写模式打开FIFO1,否则两个进程都会被阻塞
if(fdw_fifo < 0)
{
printf("fifo1写模式打开失败:%s\n",strerror(errno));
exit(0);//程序退出
}
printf("成功打开fifo1的写模式!\n");
fdr_fifo = open(".fifo2",O_RDONLY);
if(fdr_fifo < 0)
{
printf("fifo2读模式打开失败:%s\n",strerror(errno));
exit(0);//程序退出
}
printf("成功打开fifo2的读模式!\n");
}
epollfd = epoll_create(2);//创建epoll实例
if(epollfd < 0)
{
printf("epoll create error:%s\n",strerror(errno));
goto Cleanup;
}
//初始化注册标准输入
ctl_event.events = EPOLLIN;//设置为读
ctl_event.data.fd = STDIN_FILENO;
rvctl = epoll_ctl(epollfd,EPOLL_CTL_ADD,STDIN_FILENO,&ctl_event);
if(rvctl < 0)
{
printf("ctl stdin 失败:%s\n",strerror(errno));
close(epollfd);
goto Cleanup;
}
//初始化注册fdr_fifo
ctl_event.events = EPOLLIN;//设置为读
ctl_event.data.fd = fdr_fifo;
rvctl = epoll_ctl(epollfd,EPOLL_CTL_ADD,fdr_fifo,&ctl_event);
if(rvctl < 0)
{
printf("ctl fdr_fifo 失败:%s\n",strerror(errno));
close(epollfd);
goto Cleanup;
}
//epoll处理两个文件描述符:标准输入和fdr_fifo
while(!g_stop)
{
printf("开始监听文件描述符...\n");
rvwait = epoll_wait(epollfd,rv_event,2,-1);//-1:永远阻塞直到有文件描述被触发时返回
if(rvwait < 0)
{
printf("epoll_wait error:%s\n",strerror(errno));
close(epollfd);
goto Cleanup;
}
else if(rvwait == 0)
{
printf("wait timeout!\n");
continue;
}
printf("监听返回!rvwait=%d\n",rvwait);
if(rvwait > 0)
{
for(i = 0; i < rvwait; i++)
{
if(rv_event[i].data.fd == STDIN_FILENO)//标准输入有数据
{
memset(buf, 0, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
rv = write(fdw_fifo, buf, strlen(buf));
if(rv < 0)
{
printf("向管道写入数据错误:%s\n",strerror(errno));
close(rv_event[i].data.fd);
goto Cleanup;
}
}
else //fdr_fifo文件描述可读
{
printf("文件描述[%d]状态是[%d]!\n",rv_event[i].data.fd,rv_event[i].events);
if(rv_event[i].events & EPOLLIN)
{
printf("进入if...\n");
memset(buf, 0, sizeof(buf));
rv = read(fdr_fifo, buf, sizeof(buf));
if( rv < 0)
{
printf("从fdr_fifo读数据失败: %s\n", strerror(errno));
close(rv_event[i].data.fd);
goto Cleanup;
}
else if( 0 == rv ) /* 如果从管道上读到字节数为0,说明管道的写端已关闭 */
{
printf("管道另一端已关闭\n");
close(rv_event[i].data.fd);
goto Cleanup;
}
printf("从管道读到[%d]字节数据:%s",rv, buf);
}
else if(rv_event[i].events & EPOLLHUP)//文件描述被挂断
{
printf("管道另一端已关闭\n");
close(rv_event[i].data.fd);
goto Cleanup;
}
}
}
}
}
Cleanup:
exit(0);
}
在不同的终端运行上述程序:
4、命名socket
-
命名socket用于同一主机中不同进程间的通信,网络socket基于TCP/IP协议栈用于不同主机、不同网络间的通信。它们在实现上的主要区别在于网络socket绑定IP地址和端口号,命令socket只需要一个特定的路径就可以进行通信,免去了网络协议的封装,在数据传输速率上远超于网络socket。
-
命名socket又称unix域socket,提供了类似于TCP的字节流套接字和类似于UDP的数据包套接字,数据报服务是稳定可靠的,不会丢失数据。
-
socket结构体不再是struct sockaddr_in,而是:
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 路径名 */
};
- 命名socket客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCKET_PATH "socket_path"//命名socket通信路径
#define MSG_STR "Hello,I am client, nice to meet you!"
#define SIZE 50
int main(int argc, char **argv)
{
int sockfd = -1;
struct sockaddr_un server_addr;
char buff[SIZE];
int rv = -1;
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);//创建套接字,第三参数0会自动匹配与参数二适应的协议
if(sockfd < 0)
{
printf("socket error:%s\n",strerror(errno));
return 0;
}
//初始化服务器
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(SOCKET_PATH)-1);//减一是因为去掉字符串中‘\0’
if( connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)//连接服务器
{
printf("connect error:%s\n",strerror(errno));
goto Cleanup;
}
if( (rv = write(sockfd, MSG_STR, strlen(MSG_STR))) < 0 )//向服务器写数据
{
printf("write error:%s\n",strerror(errno));
goto Cleanup;
}
printf("Send [%d] bytes to named_server successfully!\n",rv);
memset(buff, 0, sizeof(buff));
if((rv = read(sockfd, buff, sizeof(buff))) < 0)
{
printf("read error:%s\n",strerror(errno));
goto Cleanup;
}
else if(rv == 0)
{
printf("named_server disconnct!\n");
}
printf("%s\n",buff);
Cleanup:
close(sockfd);
return 0;
}
- 命名socket服务端代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#define SOCKET_PATH "socket_path"//命名socket通信路径
#define LINK_NUMS 5//连接数
#define SIZE 50
int main(int argc, char **argv)
{
int sockfd = -1;
int clientfd = -1;
struct sockaddr_un server_addr;//服务器配置结构体
struct sockaddr_un client_addr;//缓存客户端信息结构体
int client_len;
int rv = -1;
char buff[SIZE];
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);//创建套接字,第三参数0会自动匹配与参数二适应的协议
if(sockfd < 0)
{
printf("socket error:%s\n",strerror(errno));
exit(0);
}
//判断socket文件是否存在
if(!access(SOCKET_PATH, F_OK))
{
unlink(SOCKET_PATH);
}
//初始化服务器
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(SOCKET_PATH)-1);//减一是因为去掉字符串中‘\0’
if(bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
printf("bind error:%s\n",strerror(errno));
goto Cleanup;
}
listen(sockfd, LINK_NUMS);
printf("Starting to wait client to link...\n");
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);//开始等待连接
if(clientfd < 0)
{
printf("accept error:%s\n",strerror(errno));
goto Cleanup;
}
//读数据
memset(buff, 0, sizeof(buff));
rv = read(clientfd, buff, sizeof(buff));
if(rv < 0)
{
printf("named_server read error:%s\n",strerror(errno));
goto Cleanup;
}
else if(rv == 0)
{
printf("named_client disconnect!\n");
goto Cleanup;
}
printf("named_server received [%d] bytes from named_client:%s\n",rv,buff);
//回显
rv = write(clientfd, "named_server received over!", 28);
if(rv < 0)
{
printf("named_server write error:%s\n",strerror(errno));
goto Cleanup;
}
Cleanup:
close(sockfd);
close(clientfd);
unlink(SOCKET_PATH);
exit(0);
}
5、信号量
- 信号量就是我们在操作系统系统中学过的,用于进程之间同步的一种操作,这种操作称为PV操作。信号量和信号都可以实现进程间的同步,但是实现的机制不同,信号(signal)通过信号处理器来操作的,而信号量通过PV操作实现。
- PV操作的实质其实就是对临界资源的控制,让不同的进程能够有序充分地访问临界资源,避免产生死锁和资源短缺问题。
- P操作:进程进入临界资源时,信号量值减一,信号量值为0说明可用资源已经没有了,进程将进入等待队列。
- V操作:进程使用完资源后,出临界区,信号量值加一,此时正在等待的进程可以进入临界区使用临界资源。
- linux下PV操作的函数简单总结,具体使用方法man一下就可以
ftok()获取IPC关键字,在共享内存、消息队列中都需要用到键值,这个键值独一无二,直接赋值可能与其他冲突,所以我们最好使用这个函数生成。
semget()创建或获取信号量
semctl()初始化信号集或删除信号集
semop()PV操作函数
- 一个父子进程同步的例子,如果子进程线运行,父进程由于信号量为赋值而阻塞直到子进程运行结束后才继续运行
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_PATH "../apue"//路径必须存在
#define PROJ_ID 10//8bit
//定义一个信号量集联合
union semun
{
int val;
struct semid_us *buf;
unsigned short *arry;
};
/*
功能:信号量初始化
参数:无
返回:
成功返回信号量值
失败返回-1
*/
int semaphore_init(void)
{
key_t key = -1;
int sem_val = -1;
union semun sem_union;
int rv = -1;
key = ftok(FTOK_PATH,PROJ_ID);//获取IPC关键字
if(key < 0)
{
printf("ftok error:%s\n",strerror(errno));
return -1;
}
sem_val = semget(key, 1, IPC_CREAT|0644);//获取信号量,当信号量不存在时,创建一个心的
if(sem_val < 0)
{
printf("semget error:%s\n",strerror(errno));
return -1;
}
rv = semctl(sem_val, 0, SETVAL, sem_union);//初始化信号量集,设置信号量集中一个单独信号量的值。
if(rv < 0)
{
printf("semctl error:%s\n",strerror(errno));
return -1;
}
return sem_val;
}
/*
功能:从系统中删除信号量集合
参数:
sem_val:信号量值
返回:
成功返回0;
失败返回-1;
*/
int semaphore_delete(int sem_val)
{
union semun sem_union;
if(semctl(sem_val, 0, IPC_RMID, sem_union) < 0)
{
printf("semctl delete error:%s\n",strerror(errno));
return -1;
}
return 0;
}
/*
功能:P操作
参数:
sem_val:信号量值
返回:
成功返回0
失败返回-1
*/
int semaphore_p(int sem_val)
{
struct sembuf _sembuf;//信号量操作结构体
//初始化结构体
_sembuf.sem_num = 0;//信号集中第一个信号的编号
_sembuf.sem_op = -1;//P操作
_sembuf.sem_flg = SEM_UNDO;//程序结束时,信号值恢复
if(semop(sem_val, &_sembuf, 1) < 0)
{
printf("semop p error:%s\n",strerror(errno));
return -1;
}
return 0;
}
/*
功能:V操作
参数:
sem_val:信号量值
返回:
成功返回0
失败返回-1
*/
int semaphore_v(int sem_val)
{
struct sembuf _sembuf;//信号量操作结构体
//初始化结构体
_sembuf.sem_num = 0;//信号集中第一个信号的编号
_sembuf.sem_op = 1;//V操作
_sembuf.sem_flg = SEM_UNDO;//程序结束时,信号值恢复
if(semop(sem_val, &_sembuf, 1) < 0)
{
printf("semop p error:%s\n",strerror(errno));
return -1;
}
return 0;
}
int main(int argc, char **argv)
{
int pid = -1;
int sem_val = -1;
sem_val = semaphore_init();
if(sem_val < 0)
{
exit(0);
}
pid = fork();
if(pid < 0)
{
printf("fork error:%s\n",strerror(errno));
exit(0);
}
else if(pid == 0)
{
printf("child is working...\n");
sleep(5);
printf("child working over!\n");
semaphore_v(sem_val);
exit(0);//子进程退出
}
printf("father is blocking...\n");
semaphore_p(sem_val);//若父进程线运行,p操作使信号量小于0,父进程阻塞直到子进程V操作
printf("father woking over...\n");
semaphore_delete(sem_val);//销毁信号集
printf("program exit!\n");
return 0;
}
6、共享内存
- 共享内存就是多个进程可以访问通一块内存空间,共享内存映射到各个进程的用户空间,所以进程间的数据交换不涉及内核,是IPC中最快的一种方式。
- 任一进程对共享内存中数据变量的修改,另一进程可以看到。
- 用到的函数简述
key_t ftok(const char *pathname, int proj_id):生成键值,相当于身份证
int shmget(key_t key, size_t size, int shmflg):该函数用来创建共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg):用来启动对该共享内存的访问,并映射到进程地址空间
int shmdt(const void *shmaddr):于将共享内存从当前进程中分离
int shmctl(int shmid, int cmd, struct shmid_ds *buf):于控制共享内存
struct shmid_ds
{
uid_t shm_perm.uid;//共享内存用户ID
uid_t shm_perm.gid;//共享内存组ID
mode_t shm_perm.mode;//共享内存模式
}
- 一个例子:一个进程创建共享内存,另一个进程访问共享内存。
shared_mem_create.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/time.h>
#define FTOK_PATH "../apue"
#define PROJ_ID 10
//定义共享结构体
typedef struct
{
int money;
char ID[6];
}bank_card_t;
void seconds_delay(unsigned seconds)
{
struct timeval tv;
tv.tv_sec=seconds;
tv.tv_usec=0;
int err;
do{
err=select(0,NULL,NULL,NULL,&tv);
}while(err<0 && errno==EINTR);
}
int main(int argc, char **argv)
{
key_t key = -1;
bank_card_t *b_card;//银行卡
int shmid = -1;//内存ID
int i ;
key = ftok(FTOK_PATH, PROJ_ID);//获取身份证
if(key < 0)
{
printf("ftok error:%s\n",strerror(errno));
exit(0);
}
shmid = shmget(key, sizeof(bank_card_t), IPC_CREAT|0666);//分配共享内存
if(shmid < 0)
{
printf("shmget error:%s\n",strerror(errno));
exit(0);
}
b_card = shmat(shmid, NULL, 0);//启动共享内存并由系统选择共享内存地址,且连接到当前进程的地址空间
if(b_card == (void *)-1)
{
printf("shmat error:%s\n",strerror(errno));
exit(0);
}
//填充共享内存
strcpy(b_card->ID,"a1234");
b_card->money = 1000;
for(i = 0; i < 4; i++)
{
b_card->money += 500;
printf("账号:%s 余额:%d\n",b_card->ID,b_card->money);
seconds_delay(2);//延时是因为给等待另外一个进程读内容
}
if(shmdt(b_card) < 0)//从当前进程分离共享内存
{
printf("shmdt error:%s\n",strerror(errno));
exit(0);
}
/* if(shmctl(shmid, IPC_RMID, NULL))//删除共享内存
{
printf("shmdt error:%s\n",strerror(errno));
exit(0);
}
*/
return 0;
}
shared_mem_visit.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/time.h>
#define FTOK_PATH "../apue"
#define PROJ_ID 10
//定义共享结构体
typedef struct
{
int money;
char ID[6];
}bank_card_t;
void seconds_delay(unsigned seconds)
{
struct timeval tv;
tv.tv_sec=seconds;
tv.tv_usec=0;
int err;
do{
err=select(0,NULL,NULL,NULL,&tv);
}while(err<0 && errno==EINTR);
}
int main(int argc, char **argv)
{
key_t key = -1;
bank_card_t *b_card;//银行卡
int shmid = -1;//内存ID
int i ;
key = ftok(FTOK_PATH, PROJ_ID);//获取身份证
if(key < 0)
{
printf("ftok error:%s\n",strerror(errno));
exit(0);
}
shmid = shmget(key, sizeof(bank_card_t), IPC_CREAT|0666);//分配共享内存
if(shmid < 0)
{
printf("shmget error:%s\n",strerror(errno));
exit(0);
}
b_card = shmat(shmid, NULL, 0);//启动共享内存并由系统选择共享内存地址,且连接到当前进程的地址空间
if(b_card == (void *)-1)
{
printf("shmat error:%s\n",strerror(errno));
exit(0);
}
for(i = 0; i < 4; i++)
{
printf("账号:%s 余额:%d\n",b_card->ID,b_card->money);
seconds_delay(2);
}
if(shmdt(b_card) < 0)//从当前进程分离共享内存
{
printf("shmdt error:%s\n",strerror(errno));
exit(0);
}
if(shmctl(shmid, IPC_RMID, NULL))//删除共享内存
{
printf("shmdt error:%s\n",strerror(errno));
exit(0);
}
return 0;
}
运行程序时,必须在不同的终端运行,如果当create运行结束后运行visit,余额只会3000。
7、消息队列
消息队列简称MQ,可以用下面形象的图描述消息队列:放入消息、取消息。
- linux内核通过链表实现消息队列,提供了进程之间交换数据的方法,进程之间发送和接收的是数据块,也就是一个结构体,结构体可以包含各种类型的数值。
- 在内核中,可以创建和维护的消息队列的数量有限(MSGMNI),每个消息队列的长度有限(MSGMAX)以及每个消息队列总的字节数也有限(MSGMNB)。
- linux下通过消息队列实现两进程间的通信,一般步骤如下:
(1)、创建消息
(2)、发送消息
(3)、接收消息
(4)、释放消息队列 - 消息队列API简述:
key_t ftok(const char *pathname, int proj_id):获取键值
int msgget(key_t key, int msgflg):创建消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg):发送消息体
第二个参数必须它必须小于系统规定的上限值,ipcs -ql : 显示消息队列的限制信息,其次必须以一个long int长整数开始,接收者函数将利用这个长整数确定消息的类型,其参考类型定义形式如下:
typedef struct s_msgbuf
{
long mtype;
char mtext[512];
} t_msgbuf;
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg):接收消息体
int msgctl(int msqid, int cmd, struct msqid_ds *buf):操作消息体,比如删除消息体
- 一个简单的例子:消息体
msg_send.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define FTOK_PATH "../apue"
#define PROJ_ID 10
//定义消息体
typedef struct
{
long msg_type;//必须以long开始,接收函数根据它确定消息类型
char msg_val[512];
}msg_body_t;
int main(int argc, char **argv)
{
key_t key = -1;
int msg_id = -1;
msg_body_t msg;
key = ftok(FTOK_PATH, PROJ_ID);//生成身份证
if(key < 0)
{
printf("ftok error:%s\n",strerror(errno));
exit(0);
}
msg_id = msgget(key, IPC_CREAT|0666);//创建消息队列
if(msg_id < 0)
{
printf("msgget error:%s\n",strerror(errno));
exit(0);
}
//初始化消息体
msg.msg_type = (int)key;
strcpy(msg.msg_val ,"hi,boy,good afternoon!");
//发送消息
if(msgsnd(msg_id, &msg, sizeof(msg.msg_val), IPC_NOWAIT) < 0)
{
printf("msgsnd error:%s\n",strerror(errno));
exit(0);
}
// msgctl(msg_id, IPC_RMID, NULL);//销毁消息队列,发送端在接收端没接收之前不能销毁队列,否则基金接收不到消息
return 0;
}
msg_receive.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define FTOK_PATH "../apue"
#define PROJ_ID 10
//定义消息体
typedef struct
{
long msg_type;//必须以long开始,接收函数根据它确定消息类型
char msg_val[512];
}msg_body_t;
int main(int argc, char **argv)
{
key_t key = -1;
int msg_id = -1;
msg_body_t msg;
int msgtype;
key = ftok(FTOK_PATH, PROJ_ID);//生成身份证
if(key < 0)
{
printf("ftok error:%s\n",strerror(errno));
exit(0);
}
msg_id = msgget(key, IPC_CREAT|0666);//创建消息队列
if(msg_id < 0)
{
printf("msgget error:%s\n",strerror(errno));
exit(0);
}
msgtype = (int)key;
//发送消息
if(msgrcv(msg_id, &msg, sizeof(msg.msg_val), msgtype, IPC_NOWAIT) < 0)
{
printf("msgsnd error:%s\n",strerror(errno));
exit(0);
}
printf("Receive Message: %s\n", msg.msg_val);
msgctl(msg_id, IPC_RMID, NULL);//销毁消息队列
return 0;
}
先运行发送端:
在运行接收端: