前言:Linux系统编程的基础系列文章,随着不断学习会将一些知识点进行更新,前期主要是简单了解和学习,本期主要是进程间的通信。
进程间通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核。
- 在内核中开辟一块缓冲区;
- 进程1把数据从用户空间拷到内核缓冲区;
- 进程2再从内核缓冲区把数据读走。
内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
pipe管道
#include <unistd.h>
int pipe(int filedes[2]);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int fd[2];///< fd[0]读端; fd[1]写端
char str[40] = "hello pipe.\n";
char buf[40];
pid_t pid;
if (pipe(fd) < 0) {
perror("pipe");
exit(1);
}
pid = fork();
///< 父写子读
if (pid > 0) {
sleep(2);
///< 父进程里,关闭父读
close(fd[0]);
write(fd[1], str, strlen(str));
close(fd[1]);
wait(NULL);
}
else if (pid==0) {
///< 子进程里,关闭子写
close(fd[1]);
int len = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd[0]);
}
else {
perror("fork");
exit(1);
}
return 0;
}
管道只能作用于有血缘关系的进程之间,通过fork来传递,即子进程将会继承父进程的文件描述符
pipe建立过程
- 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
先创建管道(pipe),再创建(fork)。
注意事项
- 写关闭,读端读完管道里内容时,再次读,返回0,相当于读到 EOF
- 写端未关闭,写端暂时无数据,读端读完管道里数据时,再次读,阻塞
- 读端关闭,写端写管道,会产生SIGPIPE信号,写进程默认会终止进程
- 读端未读,当写端写满管道后,再次写,阻塞
管道大小
fpathconf(int fd, int name)测试管道缓冲区大小,_PC_PIPE_BUF
#include <stdio.h>
#include <unistd.h>
int main(void) {
int fd[2];
pipe(fd);
printf("pipe buf %ld.\n", fpathconf(fd[0], _PC_PIPE_BUF));
return 0;
}
pipe buf 4096.
非阻塞管道
非阻塞管道, fcntl函数设置O_NONBLOCK标志
flags = fcntl(fd[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd[0], F_GETFL, flags);
fifo有名管道
解决无血缘关系的进程通信。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
fifo_w-test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char *argv[]) {
int fd;
char buf[40] = "hello fifo.\n";
if (argc < 2) {
printf("./fifo_w-test.\n");
exit(1);
}
fd = open(argv[1], O_WRONLY);
if (fd < 0) {
perror("open");
exit(1);
}
write(fd, buf, strlen(buf));
close(fd);
return 0;
}
fifo_r-test.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char *argv[]) {
int fd;
char buf[40];
if (argc < 2) {
printf("./fifo_w-test.\n");
exit(1);
}
fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
int len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd);
return 0;
}
./fifo_w-test fifo-test
./fifo_r-test fifo-test
hello fifo.
prw-rw-r-- 1 kudio kudio 0 Mar 22 19:59 fifo-test
并且可以发现,管道并没有实际存储,只是进行了对内核里缓冲区的索引
- 当只写打开FIFO管道时,如果没有FIFO没有读端打开,则open写打开会阻塞。
- FIFO内核实现时可以支持双向通信。(pipe单向通信,因为父子进程共享同一个file结构体)
- FIFO可以一个读端,多个写端;也可以一个写端,多个读端。(请测试
内存共享映射
mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针来做而不需要read/write函数。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
参数如下
- void *addr
- 如果addr参数为NULL,内核会自己在进程地址空间中选择合适的地址建立映射。
- 如果addr不是NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择addr之上的某个合适的地址开始映射。
- size_t length
- 需要映射的那一部分文件的长度。、
- int prot
- PROT_EXEC 表示映射的这一段可执行,例如映射共享库
- ROT_READ 表示映射的这一段可读
- PROT_WRITE 表示映射的这一段可写
- PROT_NONE 表示映射的这一段不可访问
- int flags
- MAP_SHARED 多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化。
- MAP_PRIVATE 多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去。
- int fd
- 文件描述符
- off_t offset
- 偏移量(4096的整数倍)
- 如果mmap成功则返回映射首地址,如果出错则返回常数MAP_FAILED。
- 当进程终止时,该进程的映射内存会自动解除,也可以调用munmap解除映射。munmap成功返回0,出错返回-1。
- 进程未终止时,且未使用munmap,则会造成内存泄漏。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
int fd;
fd = open("mmap_str", O_RDWR);
if (fd < 0) {
perror("open");
exit(1);
}
int len = lseek(fd, 0, SEEK_END);
int *mmap_addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mmap_addr == MAP_FAILED) {
perror("mmap");
exit(1);
}
mmap_addr[0] = 0x30313233;
munmap(mmap_addr, len);
return 0;
}
od -tx1 -tc mmap_str
0000000 48 65 6c 6c 6f 20 6d 6d 61 70 2e 0a
H e l l o m m a p . \n
0000014
od -tx1 -tc mmap_str
0000000 33 32 31 30 6f 20 6d 6d 61 70 2e 0a
3 2 1 0 o m m a p . \n
0000014
通过上述实验,可以发现mmap是shared机制,修改内存的时候也会修改磁盘文件,修改磁盘文件也会修改内存。
mmap_w.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#define MAPLEN 0x1000
void sys_err(char *str, int exit_num) {
perror(str);
exit(exit_num);
}
int main(int argc, char *argv[]) {
char *mmap_res = NULL;
int i = 0;
int fd = 0;
if (argc < 2) {
printf("./mmap-A filename.\n");
exit(1);
}
fd = open(argv[1], O_RDWR | O_CREAT, 0777);
if (fd < 0)
sys_err("open", 1);
if ( lseek(fd, MAPLEN-1, SEEK_SET) < 0 )
sys_err("lseek", 3);
if ( write(fd, "\0", 1) < 0 )
sys_err("write", 4);
mmap_res = mmap(NULL, MAPLEN, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmap_res == MAP_FAILED)
sys_err("mmap", 2);
close(fd);
while (1) {
sprintf(mmap_res, "hello:%d", i++);
sleep(1);
}
munmap(mmap_res, MAPLEN);
return 0;
}
mmap_r.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#define MAPLEN 0x1000
void sys_err(char *str, int exit_num) {
perror(str);
exit(exit_num);
}
int main(int argc, char *argv[]) {
char *mmap_res = NULL;
int fd = 0;
if (argc < 2) {
printf("./mmap-A filename.\n");
exit(1);
}
fd = open(argv[1], O_RDWR);
if (fd < 0)
sys_err("open", 1);
mmap_res = mmap(NULL, MAPLEN, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmap_res == MAP_FAILED)
sys_err("mmap", 2);
close(fd);
while (1) {
printf("%s\n", mmap_res);
sleep(1);
}
munmap(mmap_res, MAPLEN);
return 0;
}
./mmap_w mmap_wr
./mmap_r mmap_wr
hello:1
hello:2
hello:3
hello:4
hello:5
hello:6
hello:7
查看mmap_wr,可发现磁盘内容也被更改。
-
进程间通信的时候,如果一直创建共享内存空间,最终会导致磁盘空间浪费,所以应该设计成临时文件,在读完时候,close(fd)后unlink(argv[1])
-
用于进程间通信时,一般设计成结构体,来传输通信的数据
-
进程间通信的文件,应该设计成临时文件
-
当报总线错误时,优先查看共享文件是否有存储空间