1.进程通信概念
进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
2.进程通信的方式
3.进程通信-pipe
管道分为有名管道和无名管道;
- 无名管道:是一种半双工的通信方式,数据只能单向流动(即一方只能写,另一方只能读),而且只能在具有亲缘关系的进程间使用,进程的亲缘关系一般指的是父子关系。
- 有名管道:也是一种半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3.1无名管道
无名管道也成为匿名管道,通过调用pipe()函数创建:
#include <unistd.h>
//返回:成功返回0,出错返回-1
int pipe (int fd[2]);
fd参数返回两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。fd[1]的输出是fd[0]的输入。下图描述了无名管道的通信实现过程:
代码实现:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
int main()
{
int fd[2];
int ret = pipe(fd); //创建管道
if(ret == -1) {
perror("pipe error\n");
return -1;
}
//创建子进程,fork调用一次,返回两次,子进程返回0,父进程返回子进程ID
pid_t id = fork();
if(id == 0) { //子进程执行动作
close(fd[0]); //关闭读
char* child="I am child!";
for(int i = 0; i < 5; ++i) {
write(fd[1], child, strlen(child) + 1);
sleep(2);
}
}
else if(id > 0) { //父进程执行动作
close(fd[1]); //关闭写
char msg[100];
for(int j = 0; j < 5; ++j) {
memset(msg,'\0',sizeof(msg));
ssize_t s = read(fd[0], msg, sizeof(msg));
if(s > 0) {
msg[s-1] = '\0';
}
printf("%s\n",msg);
}
}
else {
perror("fork error\n");
return -1;
}
return 0;
}
管道读取数据的四种情况分析:
读取数据情况 | 不同情况分析 |
---|---|
读端(fd[0])不读且未关闭,写端(fd[1])一直写 | 管道数据满时,再次调用write()往管道写数据时,将会被堵塞直到数据被读出才返回 |
写端(fd[1])不写且未关闭,读端(fd[0])一直读 | 读取完管道剩余数据后,再次调用read()读取管道数据时,将会被堵塞直到有数据可读才返回 |
读端(fd[0])一直读且未关闭,写端(fd[1])写部分数据后不写了且关闭 | 读取完管道剩余数据后,再次调用read()读取管道数据时,将会返回0(像读到文件末尾一样),此时不会堵塞。 |
写端(fd[1])一直写且未关闭,读端(fd[0])读了部分数据不读了且关闭 | 读端(fd[0])关闭后,再次调用write()往管道写数据时,此时会收到SIGPIPE信号,通常会导致进程异常终止 |
(如果一个管道读端一直在读数据,而管道写端的引⽤计数⼤于0决定管道是否会堵塞,引用计数大于0,只读不写会导致管道堵塞。如果一个管道写端一直在写数据,而管道读端的引用计数大于0决定是否会堵塞,引用计数大于0,只写不读会导致管道堵塞)
无名管道特点:
- 管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
- 管道只允许单向通信。
- 管道内部保证同步机制,从而保证访问数据的一致性。
- 面向字节流,管道容量是64K,即65535字节
- 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。
3.2有名管道
有名管道,即FIFO,遵循先进先出原则,通过调用mkfifo()函数创建:
#include <sys/types.h>
#include <sys/stat.h>
/*
*参数:
- filename 有名管道文件名(包括路径);
- mode 权限(读写0666)
*成功返回 0 ,失败返回-1 并设置 errno 号
*/
int mkfifo(const char *filename, mode_t mode)
对有名管道的操作是通过文件IO 中的open read write 等操作的,
进程open打开有名管道以只读方式打开,返回的文件描述符就代表管道的读端,反之为写端。
- O_RDONLY:以只读打开,读端
- O_WRONLY:以只写打开,写端
- O_RDWR:读写方式打开
另外由于普通文件在读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能, 这里的非阻塞标志可以在open()函数中设定为O_NONBLOCK。
读进程 | 写进程 |
---|---|
阻塞打开管道且管道内没有数据,则一直阻塞到有数据 | 阻塞打开管道,则写操作将一直阻塞到数据可以被写入 |
非阻塞打开管道,则不论管道内是否有数据,进程都会立即执行,没有数据则返回0 | 非阻塞打开管道而不能写入全部数据,则读操作进行部分写入或者调用失败 |
写进程代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
char buf[50] = {0};
//创建有名管道
if(mkfifo("./fifo",0777) != 0 ) {
if(errno == EEXIST) { //已存在,或可在mkfifo前用access函数判断文件是否存在
printf("File exists\n");
}
else {
perror("mkfifo fail ");
exit(1);
}
}
int fd_fifo, fd_file;
fd_fifo = open("./fifo",O_WRONLY);//只写方式打开管道,默认阻塞
if(fd_fifo < 0) {
perror("open fifo fail: ");
exit(1);
}
fd_file = open(argv[1],O_RDONLY);//只读方式打开源文件,进行复制到管道中
if(fd_file < 0) {
perror("open source fail ");
exit(1);
}
//循环读取文件内容
ssize_t size;
while(1) {
memset(buf, 0, sizeof(buf));
size = read(fd_file,buf,50); //文件中读取数据,返回读取到多少数据
if(size <= 0) {
break;
}
write(fd_fifo,buf,size);
}
close(fd_file);//关闭读的源文件
close(fd_fifo);
return 0;
}
读进程代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
char buf[50] = {0};
//创建有名管道,或可在mkfifo前用access函数判断文件是否存在
if(mkfifo("./fifo",0777) != 0 ) {
if(errno == EEXIST) {//有名管道存在的情况
printf("File exists\n");
}
else {
perror("mkfifo fail ");
exit(1);
}
}
int fd_fifo,fd_file;
fd_fifo = open("./fifo",O_RDONLY);//读方式打开,默认阻塞
if(fd_fifo < 0) {
perror("open fifo fail: ");
exit(1);
}
//把从有名管道中读取的数据,写到文件中,只读,没有创建,清空打开
fd_file = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd_file < 0) {
perror("fd_w open fail ");
exit(1);
}
//fifo 中循环读取数据,然后写到文件中
ssize_t size;
while(1) {
memset(buf, 0, sizeof(buf));
size = read(fd_fifo,buf,50); //读有名管道内容,返回读取多少个数据
if(size <= 0) {
break;
}
write(fd_file,buf,size); //写入文件中
}
close(fd_fifo);
close(fd_file);
return 0;
}
有名管道特点:
- 有名管道可以使互不相关的两个进程互相通信。
- 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。
- 进程通过文件IO来操作有名管道
- 有名管道遵循先进先出规则
- 不支持如lseek() 等文件定位操作
4.信号
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
信号实际上是由内核发送,内核来处理收到的信号。
信号送到进程后,必须对信号做出处理(忽略,捕获,默认动作都行)。
信号不能携带大量信息,满足特定条件发生,信号也叫软中断,有可能会有延迟。
信号可通过如下方式产生:
- 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误) 命令产生,如:kill命令
信号一览表:
信号名称 | 信号作用 |
---|---|
SIGHUP(1) | 用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程 |
SIGINT(2) | 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。 |
SIGQUIT(3) | 当用户按下<ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。 |
SIGILL(4) | CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件 |
SIGTRAP(5) | 该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。 |
SIGABRT(6) | 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。 |
SIGBUS (7) | 非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。 |
SIGFPE(8) | 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。 |
SIGKILL(9) | 无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。 |
SIGUSE1(10) | 用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。 |
SIGSEGV(11) | 指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。 |
SIGUSR2(12) | 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。 |
SIGPIPE(13) | Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。 |
SIGALRM(14) | 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。 |
SIGTERM(15) | 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。 |
SIGSTKFLT(16) | Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。 |
SIGCHLD(17) | 子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。 |
SIGCONT(18) | 如果进程已停止,则使其继续运行。默认动作为继续/忽略。 |
SIGSTOP(19) | 停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。 |
SIGTSTP(20) | 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。 |
SIGTTIN(21) | 后台进程读终端控制台。默认动作为暂停进程。 |
SIGTTOU(22) | 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。 |
SIGURG(23) | 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。 |
SIGXCPU(24) | 进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。 |
SIGXFSZ(25) | 超过文件的最大长度设置。默认动作为终止进程。 |
SIGVTALRM (26) | 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。 |
SGIPROF (27) | 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。 |
SIGWINCH(28) | 窗口变化大小时发出。默认动作为忽略该信号。 |
SIGIO(29) | 此信号向进程指示发出了一个异步IO事件。默认动作为忽略。 |
SIGPWR(30) | 关机。默认动作为终止进程。 |
SIGSYS (31) | 无效的系统调用。默认动作为终止进程并产生core文件。 |
SIGRTMIN~ SIGRTMAX(34~64) | LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。 |
5.进程通信-信号量(Seamaphore)
信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),主要作为进程间以及同一个进程内不同线程之间的同步手段。
- 进程:用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
- 线程:可以用来控制多个线程对共享资源的访问,它不是用于交换大批数据,而用于多线程之间的同步。
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
- P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
- V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)
注:原子操作:单指令的操作称为原子的,单条指令的执行是不会被打断的
5.1进程信号量
进程通过信号量获得共享资源:(信号量通过同步与互斥保证访问资源的一致性。)
(1)测试控制该资源的信号量
(2)信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位
(3)若此时信号量的值为0,则进程进入挂起状态(进程状态改变),直到信号量的值大于0,若进程被唤醒则返回至第一步。
相关函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//系统建立IPC通讯(如消息队列、共享内存、信号量时)必须指定一个key值。
//通常情况下,该key值通过ftok函数得到。
//fname就是你指定的文件名(该文件必须是存在而且可以访问的)
//id是子序号,虽然为int,但是只有8个比特被使用(0-255)。
key_t ftok( char * fname, int id )
//返回:成功返回信号集ID,出错返回-1
//key是长整型(唯一非零),由ftok得到
//nsem指定信号量集中需要的信号量数目,它的值几乎总是1
//flag是一组标志,当想要当信号量不存在时创建一个新的信号量
//可以将flag设置为IPC_CREAT与文件权限做按位或操作。(**设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误**)
int semget(key_t key,int nsems,int flags)
//删除和初始化信号量
//sem_id是由semget返回的信号量标识符
//semnum当前信号量集的哪一个信号量
//cmd通常是下面两个值中的其中一个
//SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
//IPC_RMID:用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。
//如有需要第四个参数一般设置为union semnu arg;定义如下
int semctl(int semid, int semnum, int cmd, ...);
union semun
{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET 使用的缓存区
unsigned short *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};
//改变信号量的值
//sem_id是由semget返回的信号量标识符
//sops格式如下
//nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
int semop(int semid, struct sembuf *sops, size_t nops);
struct sembuf{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
代码实现:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
//创建信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if(argc > 1) {
//程序第一次被调用,初始化信号量
if(!set_semvalue()) {
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
//设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//进入临界区
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中输出数据
printf("%c", message);
//清理缓冲区,然后休眠随机时间
fflush(stdout);
sleep(rand() % 3);
//离开临界区前再一次向屏幕输出数据
printf("%c", message);
fflush(stdout);
//离开临界区,休眠随机时间后继续循环
if(!semaphore_v())
exit(EXIT_FAILURE);
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if(argc > 1)
{
//如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信号量,在使用信号量前必须这样做
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
static void del_semvalue()
{
//删除信号量
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{
//对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;//V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
5.2线程信号量
线程信号量的工作原理与进程信号量一样。
相关函数::
#include <semaphore.h>
//创建信号量
//sem一个指向sem_t变量的指针
//pshared 控制信号量的类型,值为 0 代表该信号量用于多线程间的同步,值如果大于 0 表示可以共享,用于多个相关进程间的同步
//信号量初始值
int sem_init(sem_t * sem,int pshared,unsigned int value);
//是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。
//若sem value > 0,则该信号量值减去 1, 并立即返回。
//若sem value = 0,则阻塞直到 sem value > 0,此时立即减去 1,然后返回
int sem_wait(sem_t *sem);
//函数是非阻塞的函数,它会尝试获取 sem value 值,
//如果 sem value = 0,不阻塞,而是直接返回一个错误 EAGAIN。
int sem_trywait(sem_t *sem);
//把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程
int sem_post(sem_t *sem);
//获取信号量 sem 的当前值,把该值保存在 sval,
//若有 1 个或者多个线程正在调用 sem_wait 阻塞在该信号量上,
//该函数返回阻塞在该信号量上进程或线程个数。
int sem_getvalue(sem_t *sem, int *sval);
//对用完的信号量的清理
int sem_destroy(sem_t *sem);
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <errno.h>
#define total 20
sem_t remain, apple, pear, mutex;
static unsigned int vremain = 20, vapple = 0, vpear = 0;
void *father(void *);
void *mather(void *);
void *son(void *);
void *daughter(void *);
void print_sem();
int main()
{
pthread_t fa, ma, so, da;
sem_init(&remain, 0, total);//总数初始化为20
sem_init(&apple, 0, 0);//盆子中苹果数, 开始为0
sem_init(&pear, 0, 0);//盆子中梨子数, 开始为0
sem_init(&mutex, 0, 1);//互斥锁, 初始为1
pthread_create(&fa, NULL, &father, NULL);
pthread_create(&ma, NULL, &mather, NULL);
pthread_create(&so, NULL, &son, NULL);
pthread_create(&da, NULL, &daughter, NULL);
for(;;);
return 0;
}
void *father(void *arg)
{
while(1) {
sem_wait(&remain);
sem_wait(&mutex);
printf("父亲: 放苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain--, vapple++);
printf("父亲: 放苹果之后, 剩余空间=%u, 苹果数=%u\n", vremain, vapple);
sem_post(&mutex);
sem_post(&apple);
sleep(1);
}
}
void *mather(void *arg)
{
while(1) {
sem_wait(&remain);
sem_wait(&mutex);
printf("母亲: 放梨子之前, 剩余空间=%u, 梨子数=%u\n", vremain--, vpear++);
printf("母亲: 放梨子之后, 剩余空间=%u, 梨子数=%u\n", vremain, vpear);
sem_post(&mutex);
sem_post(&pear);
sleep(2);
}
}
void *son(void *arg)
{
while(1) {
sem_wait(&pear);
sem_wait(&mutex);
printf("儿子: 吃梨子之前, 剩余空间=%u, 梨子数=%u\n", vremain++, vpear--);
printf("儿子: 吃梨子之后, 剩余空间=%u, 梨子数=%u\n", vremain, vpear);
sem_post(&mutex);
sem_post(&remain);
sleep(3);
}
}
void *daughter(void *arg)
{
while(1) {
sem_wait(&apple);
sem_wait(&mutex);
printf("女儿: 吃苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain++, vapple--);
printf("女儿: 吃苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain, vapple);
sem_post(&mutex);
sem_post(&remain);
sleep(3);
}
}
void print_sem()
{
int val1, val2, val3;
sem_getvalue(&remain, &val1);
sem_getvalue(&apple, &val2);
sem_getvalue(&pear, &val3);
printf("Semaphore: remain:%d, apple:%d, pear:%d\n", val1, val2, val3);
}
6.进程通信-消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列是UNIX下不同进程之间可实现共享资源的一种机制,UNIX允许不同进程将格式化的数据流以消息队列形式发送给任意进程,对消息队列具有操作权限的进程都可以使用msgget完成对消息队列的操作控制,通过使用消息类型,进程可以按任何顺序读信息或为消息安排优先级顺序。
相关函数:
#include <sys/msg.h>
//创建和访问一个消息队列
//key可以由ftok得到,也可以是常量IPC_PRIVATE
//oflag是读写权限的组合(用于打开时)或是IPC_CREATE或IPC_CREATE | IPC_EXCL(用于创建时)
int msgget (key_t key, int oflag) ;
//msgsnd往打开的消息队列中放置一个消息
//msqid是由msgget返回的标识符
//ptr格式化数据,由用户按下面格式定义
//length指定了待发送消息数据部分的长度
//参数flag的值可以指定为IPC_NOWAIT。这类似于文件IO的非阻塞IO标志。若消息队列已满,则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果没有指定IPC_NOWAIT,则进程阻塞直到下述情况出现为止:①有空间可以容纳要发送的消息 ②从系统中删除了此队列(返回EIDRM“标识符被删除”)③捕捉到一个信号,并从信号处理程序返回(返回EINTR)
int msgsnd (int msqid, const void *ptr, size_t length, int flag) ;
struct msg_st //ptr数据格式
{
long msg_type; //消息类型(大于0)
char text[MAX_TEXT];
};
//从打开的消息队列中读出一个消息
//msqid是由msgget返回的标识符
//ptr存放接收消息
//length指定了待接收消息数据部分的长度
//参数type指定希望从队列中读出什么样的消息:
//type == 0 返回队列中的第一个消息
//type > 0 返回队列中消息类型为type的第一个消息
//type < 0 返回队列中消息类型值小于或等于type绝对值的消息,如果这种消息有若干个。则取类型值最小的消息。
//(如果一个消息队列由多个客户进程和一个服务器进程使用,那么type字段可以用来包含客户进程的进程ID)
//flag与msgsnd一致,不在作解析
ssize_t msgrcv (int msqid, void* ptr, size_t length, long type, int flag) ;
//消息队列上的各种控制操作
//msqid是由msgget返回的标识符
//参数cmd说明对由msqid指定的队列要执行的命令:
//IPC_STAT :取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
//IPC_SET :按由buf指向结构中的值,设置与此队列相关结构中的字段。
//IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。
//buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
int msgctl (int msqid, in cmd, struct msqid_ds * buff) ;
struct msgid_ds
{
uid_t shm_perm.uid; //用户ID
uid_t shm_perm.gid; //组ID
mode_t shm_perm.mode; //模式
};
消息队列接收端代码实现:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
#define BUFSIZ 512
struct msg_st {
long int msg_type;
char text[BUFSIZ];
};
int main()
{
int running = 1;
int msgid = -1;
struct msg_st data;
long int msgtype = 0;
msgid = msgget((key_t)1234, 0666 | IPC_CREAT); //建立消息队列
if(msgid == -1) {
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}
//从队列中获取消息,直到遇到end消息为止
while(running) {
if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1) {
fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
exit(EXIT_FAILURE);
}
printf("You wrote: %s\n",data.text); //遇到end结束
if(strncmp(data.text, "end", 3) == 0)
running = 0;
}
//删除消息队列
if(msgctl(msgid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "msgctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
消息队列发送端代码实现:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>
#define MAX_TEXT 512
struct msg_st
{
long int msg_type;
char text[MAX_TEXT];
};
int main()
{
int running = 1;
struct msg_st data;
char buffer[BUFSIZ];
int msgid = -1;
msgid = msgget((key_t)1234, 0666 | IPC_CREAT); //建立消息队列
if(msgid == -1) {
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}
//向消息队列中写消息,直到写入end
while(running) {
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin); //输入数据
data.msg_type = 1; //注意2
strcpy(data.text, buffer);
//向队列发送数据
if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1) {
fprintf(stderr, "msgsnd failed\n");
exit(EXIT_FAILURE);
}
//输入end结束输入
if(strncmp(buffer, "end", 3) == 0)
running = 0;
sleep(1);
}
exit(EXIT_SUCCESS);
}
7.进程通信-共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC(进程间通信)方式,它是针对其它进程间通信方式运行效率低而专门设计的。共享内存并未提供同步机制,所以它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步与通信。
共享内存的生命周期随内核,即所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在(除非显式删除共享内存区域对象),在内核重新引导之前,对该共享内存区域对象的任何改写操作都将一直保留。简单地说,共享内存区域对象的生命周期跟系统内核的生命周期是一致的,而且共享内存区域对象的作用域范围就是在整个系统内核的生命周期之内。
linux中有两种共享内存:一种是我们的IPC通信的共享内存,另外的一种是存储映射I/O(mmap函数)共享文件
7.1 IPC共享内存
相关函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//创建共享内存
//key由ftok获得
//size共享内存的大小,它的值一般为一页大小的整数倍
//shmflg是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开,而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。
//成功返回共享内存的ID,出错返回-1
int shmget(key_t key, size_t size, int shmflg);
//操作共享内存
//shm_id是shmget函数返回的共享内存标识符
//cmd是要采取的操作,它可以取下面的三个值:
//IPC_STAT :取此共享内存的shmid_ds 结构,并将它存放在buf指向的结构中。
//IPC_SET :按由buf指向结构中的值,设置与此共享内存相关结构中的字段。
//IPC_RMID:从系统中删除该共享内存段。
//buf是一个结构指针,它指向共享内存模式和访问权限的结构
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
//挂接操作,将创建好的共享内存映射到进程的地址空间
//shm_id是由shmget函数返回的共享内存标识
//shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
//shm_flg是一组标志位,通常为0
//成功返回指向共享存储段的指针,出错返回-1
void *shmat(int shm_id, const void *shm_addr, int shmflg);
//分离操作,即分离进程和共享内存的映射,该操作不从系统中删除标识符和其数据结构
//addr参数是调用shmat时的返回值
int shmdt(const void *shmaddr);
头文件代码实现:
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#define TEXT_SZ 2048
struct shared_use_st
{
int written;//作为一个标志,非0:表示可读,0表示可写
char text[TEXT_SZ];//记录写入和读取的文本
};
#endif
读端代码实现:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;//程序是否继续运行的标志
void *shm = NULL;//分配的共享内存的原始首地址
struct shared_use_st *shared;//指向shm
int shmid;//共享内存标识符
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1) {
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1) {
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %X\n", (int)shm); //设置共享内存
shared = (struct shared_use_st*)shm;
shared->written = 0;
//读取共享内存中的数据
while(running) { //没有进程向共享内存定数据有数据可读取
if(shared->written != 0) {
printf("You wrote: %s", shared->text);
sleep(rand() % 3);
shared->written = 0; //读取完数据,设置written使共享内存段可写
if(strncmp(shared->text, "end", 3) == 0) //输入了end,退出循环(程序)
running = 0;
}
else //有其他进程在写数据,不能读取数据
sleep(1);
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, 0) == -1) {
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
写端代码实现:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[BUFSIZ + 1];//用于保存输入的文本
int shmid;
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1) {
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, (void*)0, 0);
if(shm == (void*)-1) {
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X\n", (int)shm); //设置共享内存
shared = (struct shared_use_st*)shm;
//向共享内存中写数据
while(running) {
//数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
while(shared->written == 1) {
sleep(1);
printf("Waiting...\n");
}
//向共享内存中写入数据
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
strncpy(shared->text, buffer, TEXT_SZ);
shared->written = 1; //写完数据,设置written使共享内存段可读
if(strncmp(buffer, "end", 3) == 0) //输入了end,退出循环(程序)
running = 0;
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
sleep(2);
exit(EXIT_SUCCESS);
}
7.2存储映射I/O共享文件
通过tmpfs这个文件系统来实现共享内存,tmpfs文件系的目录为/dev/shm,/dev/shm是驻留在内存 RAM当中的,因此读写速度与读写内存速度一样,/dev/shm的容量默认尺寸为系统内存大小的一半大小,使用df -h命令可以看到。但实际上它并不会真正的占用这块内存,如果/dev/shm/下没有任何文件,它占用的内存实际上就是0字节,仅在使用shm_open文件时,/dev/shm才会真正占用内存
相关函数:
//打开或创建共享内存文件,shm_open操作的文件一定是位于tmpfs文件系统里的(/dev/shm)
//name:要打开或创建的共享内存文件名,由于shm_open 打开或操作的文件都是位于/dev/shm目录的,因此name不能带路径,例如:/var/myshare 这样的名称是错误的,而 myshare 是正确的,因为 myshare 不带任何路径。如果你一定要在name添加路径,那么,请在/dev/shm目录里创建一个目录,例如,如果你想创建一个bill/myshare 的共享内存文件,那么请先在/dev/shm目录里创建 bill这个子目录,由于不同厂家发布的linux系统的tmpfs的位置也许不是/dev/shm,因此带路径的名称也许在别的环境下打开不成功。
//oflag:打开的文件操作属性:O_CREAT、O_RDWR、O_EXCL的按位或运算组合
//mode:文件共享模式,例如 0777
//返回值:成功返回fd>0, 失败返回fd<0
int shm_open(const char *name, int oflag, mode_t mode);
//mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存
//addr某个特定的地址作为起始地址,当设置为NULL,系统会在地址空间选择一块合适的内存区域
//length说的是内存段的大小
//prot是用来设定内存段的访问权限
//PROT_READ 内存段可读
//PROT_WRITE 内存段可写
//PROT_EXEC 内存段可执行
//PROT_NONE 内存段不能被访问
//flags参数控制内存段内容被修改以后程序的行为
//MAP_SHARED 进程间共享内存,对该内存段修改反映到映射文件中。
//MAP_PRIVATE 内存段为调用进程所私有。对该内存段的修改不会反映到映射文件
//MAP_ANNOYMOUS 这段内存不是从文件映射而来的。内容被初始化为全0
//MAP_FIXED 内存段必须位于start参数指定的地址处,start必须是页大小的整数倍(4K整数倍)
//MAP_HUGETLB 按照大内存页面来分配内存空间
//fd参数是用来被映射文件对应的文件描述符,用 shm_open打开或者open打开的文件
//offset映射文件相对于文件头的偏移位置,应该按4096字节对齐
//成功返回映射的内存地址指针,可以用这个地址指针对映射的文件内容进行读写操作,读写文件数据如同操作内存一样;失败则返回NULL。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//取消内存映射,munmap 只是将映射的内存从进程的地址空间撤销,如果不调用这个函数,则在进程终止前,该片区域将得不到释放。
//addr是由mmap成功返回的地址
//length是要取消的内存长度
int munmap(void *addr, size_t length);
//删除/dev/shm目录的文件,shm_unlink 删除的文件是由shm_open函数创建于/dev/shm目录的,用shm_open 创建的文件,如果不调用此函数删除,会一直存在于/dev/shm目录里,直到操作系统重启或者调用linux命令rm来删除为止
//name文件名,用/dev/shm + name 组成完整的路径也可以,但一般不要这么做,因为系统的tmpfs的位置也许不是/dev/shm
int shm_unlink(const char *name);
//重置文件大小,任何open打开的文件都可以用这个函数,不限于shm_open打开的文件
//fd文件描述符
//length重置的文件大小
int ftruncate(int fd, off_t length);
写端代码实现:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#define MMAP_DATA_SIZE 1024
#define USE_MMAP 1
int main(int argc,char * argv[])
{
char * data;
int fd = shm_open("shm-file0001", O_CREAT|O_RDWR, 0777);
if (fd < 0) {
printf("shm_open failed!\n");
return -1;
}
ftruncate(fd, MMAP_DATA_SIZE); //重置文件大小
if (USE_MMAP) {
data = (char*)mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (!data) {
printf("mmap failed\n");
close(fd);
}
sprintf(data, "This is a share memory! %d\n", fd);
munmap(data, MMAP_DATA_SIZE);
}
else {
char buf[1024];
int len = sprintf(buf,"This is a share memory by write! ! %d\n",fd);
if (write(fd, buf, len) <= 0) {
printf("write file %d failed!%d\n",len,errno);
}
}
close(fd);
//shm_unlink("shm-file0001");
return 0;
}
读端代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define MMAP_DATA_SIZE 1024
int main(int argc,char * argv[])
{
char * data;
int fd = shm_open("shm-file0001", O_RDWR, 0777);
if(fd < 0) {
printf("error open shm object\n");
return -1;
}
data = (char*)mmap(NULL, MMAP_DATA_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (!data) {
printf("mmap failed!\n");
close(fd);
return -1;
}
printf(data);
munmap(data,MMAP_DATA_SIZE);
close(fd);
return 0;
}
8.进程通信-socket
socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
创建socket,类型为AF_LOCAL或AF_UNIX,表示用于本地进程通信。由于这是基础知识,所以这里不在详解。