进程间通信(编程版)
除学linux,内容较为粗糙,如有问题,请多多指教。
此篇重点在编程上,对于理论分析较少,后续会专门分析理论。
进程间通信:管道,信号量,共享内存,消息队列,套接字。
1.管道:
1.简诉:
管道:有名管道和无名管道
有名管道:用于任意两个进程中间
无名管道:用于父子进程中间
管道文件存放在磁盘上,但是管道中的数据存放在 内存中,在程序结束的时候清楚。
管道是一个半双工的方式,在同一时刻只能一段写入,一段读出。
当管道中没有数据时,读数据会阻塞,当写端关闭后,读端会返回0;当读端先关闭的时候,写端写入数据会发生异常,收到一个SIGPIPE的信号。
2.使用:
有名管道,首先我们需要建立一个管道文件。
使用mkfifo 命令可以直接创建管道文件,也可以在代码中创建。
使用管道文件
写端: 读端:
对于管道来说,也是一个文件,这地方我们就需要使用对于文件的操作,首先需要头文件<fcntl.h>,打开文件 open()函数,然后读文件 read()函数,写文件的 write()函数,以及用完后的 close()函数。
首先是open()函数,第一个参数是,文件名,然后就是类型(只读(O_RDONLY),只写(O_WRONLY),读写(O_RDWR);管道是半双工,因此是只能是只读或者是只写,不能是读写。write()函数和read()函数 和 close()函数 就是直接使用。
在读端的read()函数判断其返回值(读取的大小)就可以知道写端是否关闭,在写端的信号就可以判断,读端是否关闭。当我们只打开写端,或者是读端,就会产生阻塞,直到匹配才可以运行。
无名管道:用于父子进程之间
创建管道用 int pipe (int pipefd[2])
我们 用一个整形数组表示,fd[0]表示读端, fd[1] 表示写端。
当我们进去的时候由父进程进行写,子进程进行读。在父进程中需要关闭读端和子进程中关闭写端。其他方法同上:
2.信号量:(头文件<sys/sem.h>)
1.简诉:
信号量,共享内存,消息队列均在内核中创建,管理,进程结束也在。
首先我们要知道一个 命令 ipcs 可以查看信号量,共享内存,消息队列。(-s 信号量,-m 共享内存 ,-q 消息队列),使用 ipcrm 可以进行删除。
临界区:同一时刻只允许一个进程(线程)访问的数据。
临界资源:访问临界资源的代码。
以及原子操作的p (对信号量-1,如果等于0,则阻塞)v(对信号量+1)
在进程中,我们需要进行封装。
首先是第一个函数sem_init()函数 这个是信号量的初始值。
int semget(key_t key, int nsems, int semflg);
Semget()函数,第一个参数就是给这块起一个别名,就是说你需要通信的进程,需要进同一个地方去,第二个参数就是几个信号量,第三个就是类型IPC_CREAT(获取)IPC_EXCL(替换) 0600文件类型(linux下一切皆文件)。当IPC_CREAT 和 IPC_EXCL一起使用的时候就是全新创建。失败的时候返回 -1,这里面的第一个参数和semid 可以参考ipcs 下的前两个。
当我们创建好了之后,我们就需要初始话我们的信号量,这里面是一个联合体,
union semun {
int val; // SETVAL使用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使用缓存区
unsigned short *array; // GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用缓存区
};
int semctl(int semid, int semnum, int cmd, ...);
给一个初始值,使用semctl()函数,第一个参数是,semid:信号集的标识符,即是信号表的索引。semnum:信号集的索引,用来存取信号集内的某个信号(下标)。cmd:需要执行的命令,有效值有
IPC_STAT //将与semid关联的内核数据结构拷贝到由arg.buf指针指向的内存区。
IPC_SET //将由arg.buf指针指向的semid_ds的一些成员写入相关联的内核数据结构,同时更新它的sem_ctime成员。
IPC_RMID //立即删除信号集,唤醒所有被阻塞的进程。
IPC_INFO //Linux特有命令,返回系统范围内关于信号集的制约和其它参数,并存放在arg.__buf指向的内存区。
接下来是pv操作
这里是一个结构体,sem_num 是下标,我们这里是一个信号量,就直接是0,sem_op 则是操作,p是-1,v是+1;sem_flg 就是当我们异常终止的时候,回滚使用 。
int semop(int semid,struct sembuf *sops,size_t nsops)
就是将我们上面的这个结构体,加给这个信号量,如果返回是-1,则表示出错。
Sem_destroy()函数则是 进行信号量的销毁,在我们使用结束后,将信号量销毁掉。
使用的依然是 semctl函数 在标志位用 IPC_RMID;
我们现在是两个进程,一个打印A,一个打印B, 两个进程同时运行,要打印AA,BB 这种成对出现的形式,不能AB这种,我们可以使用是信号量进行控制。
我们需要将a b两个程序同时后台运行,然后进行打印,这里我们在销毁的时候,只需要一个人去销毁就可以了。
3.共享内存:头文件<sys/shm.h>
1.简诉:
我们可以让多个进程共同用一个快物理内存,这时候就可以进行通信。
2.使用
写端 读端
对于共享内存的使用接口要少一点,首先是shmgt()函数,
int shmget(key_t key, size_t size, int shmflg);
类型于信号量的操作,
void *shmat(int shmid, const void *shmaddr, int shmflg);于这个共享内存建立连接
第一个shmid,第二个就是地址,给个NULL,让系统进行分配,第三个 0 就是默认可读可写。返回值是一个void* 的一个指针,指向了指向共享内存第一个节点。
int shmdt(const void *shmaddr); 这里就传我们的shmat的返回值,共享内存的第一个节点。
int shmctl(int shmid, int cmd, struct shmid_ds *buf); 销毁这个共享内存。
但是因为共享内存是一个物理地址,当我们创建了这个共享内存后,读就可以一直读,写就是随便写,共享内存并不能进行控制,这时候我们就需要信号量等机制来将进行同步操作。当我们运行程序的时候:如下
4.消息队列:头文件<sys/msg.h>
简诉:
- 消息一个一个存放在一个队列中,一段就行发送消息,一段获取消息。
- 每一个消息有一个消息的类型。
使用:
写端: 读端:
首先是一个结构体mess 中 第一个 long int type 是必须要的 代表的是消息的类型,并且要>=1;后面的数据就是自己设计的,不做要求。
首先msgget()函数,和之前的信号量这些是一样的。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
向消息队列里面写数据,第二个就是消息结构体,第三个就是消息的大小,第四个就是标志 0就是默认,IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回,IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
接收消息队列的消息,第二个就是消息结构体,第三个是大小,第四个就是消息的类型,第五个接收的状态。0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢
这里我们需要知道的就是 在接收的时候,我们可以选择消息的类型,如果
0:接收第一个消息
>0:接收类型等于msgtyp的第一个消息
<0:接收类型等于或者小于msgtyp绝对值的第一个消息
当我们需要的消息没有的时候,就会阻塞。
第五个就是消息的大小,最后一个则是有多少个消息。