1- 进程相关概念


1.1 进程

程序:死的。只占用磁盘空间。 ——剧本。

进程;活的。运行起来的程序。占用内存、CPU等系统资源。 ——演出。

1.2并发

fca1331cbb0808aed75dfaa50be929adoublecpu

  • 单道程序设计

    所有程序一个一个排队执行,若A阻塞,B只能等待

  • 多道程序设计

    在管理程序控制之下,多个独立程序相互穿插运行。多道程序设计必须有硬件基础作为保证。

1.3 并发和并行

并行是宏观上并发,微观上串行

1.4 CPU和MMU

MMC.png

存储介质自下而上,容量越小,速度越快。一个寄存器大小为4K。箭头表示一条指令的执行流程,从硬盘的代码文件开始,到内存中执行程序,在存入缓存,写入寄存器,发送到CPU内部处理。

1.5 虚拟内存和物理内存映射关系

cdab6e01373c3b4a67c1654fd1b48c87.

c8d8c2a6b31315403e9f68429a325dc2

MMU修改内存访问级别,进行权级切换耗费时间,这就是为什么用户空间访问内核空间的过程非常耗时间。

1.6 PCB进程控制块

struct task struct结构体部分内部成员

  • 进程ID(PID)

  • 文件描述符表

  • 进程状态:初始态、就绪态、运行态、挂起态、终止态。

  • 进程切换时需要保存和恢复的一些CPU寄存器

  • 描述虚拟地址空间的信息

  • 描述控制终端的信息

  • 当前工作目录位置

  • umask掩码 (进程的概念)

  • 信号相关信息资源。

  • 用户id和组id

7620eea7eb36d9af5f8a4fab52c09880

1.7 环境变量

echo $PATH      # 查看环境变量

path环境变量里记录了一系列的值,当运行一个可执行文件时,系统会去环境变量记录的位置里查找这个文件并执行。

echo $TERM      # 查看终端
echo $LANG      # 查看语言
env             # 查看所有环境变量

进程控制


2- fork 函数


用来创建新进程

43162c0d72a829ebd96e88e3a09f22c3.fork

2.1 函数原型

pid_t fork(void);
// 例如:pid_t pid = fork();

2.2 返回值

fork() 函数返回两次

  • 一次在父进程中返回子进程的进程ID(PID)

  • 一次在子进程中返回0

通过这种方式,父进程和子进程可以根据返回值来区分自己是父进程还是子进程。

  • 失败返回-1

2.3 示例

通过创建进程,打印自己是父进程还是子进程

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
​
int main(int argc, char *argv[])
{
    printf("before fork-1-\n");
    printf("before fork-2-\n");
    printf("before fork-3-\n");
    printf("before fork-4-\n");
​
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("---child is created\n");
    }
    else if(pid > 0)
    {
        printf("---parent process: my child is %d\n", pid);
    }
​
    printf("==============end of file\n");
​
    return 0;
}

2.4 getpid 和 getppid

用来获取进程ID

2.4.1 函数原型
pid_t getpid();     // 获取当前进程id
pid_t getppid();    // 获取当前进程的父进程id
2.4.2 示例

分别在子父进程获取当前PID和父进程PID

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
​
int main(int argc, char *argv[])
{
    printf("before fork-1-\n");
    printf("before fork-2-\n");
    printf("before fork-3-\n");
    printf("before fork-4-\n");
​
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("---child process: pid = %d, parent_pid = %d\n", getpid(),getppid());
    }
    else if(pid > 0)
    {
        printf("---parent process: my child is %d, my pid = %d, parent_pid = %d\n", pid, getpid(),getppid());
    }
​
    printf("==============end of file\n");
​
    return 0;
}

pid_ps_result.png

pid_result.png

这里父进程的父进程PID表示的是当前bashPID

2.5 循环创建多个子进程

通过循环给一个父进程创建5个子进程

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
​
int main(int argc, char *argv[])
{
    int i;
    pid_t pid;
​
    for (i = 0; i < 5; i++)
    {
        if ((pid = fork()) == 0)    // 这里必须要判断是否为子进程,不然子进程和父进程会一起都创建出子进程
        {
            break;
        }
        if(i == 5)
        {
            sleep(i);
            printf("i'm parent\n");
        }
        else
        {
            sleep(i);
            printf("i'm %dth child\n", i+1);
        }
        
    }
​
    return 0;
}

2.6 父子进程共享哪些内容

父子进程相同:(0-3G用户空间)

  • .data

  • .text

  • 环境变量

  • 用户ID

  • 宿主目录

  • 进程工作目录

  • 信号处理方式

父子进程不同:

  • 进程id

  • fork返回值

  • 各自的父进程

  • 进程创建运行时间

  • 闹钟 ( 定时器 )

  • 未决信号集

原则

读时共享、写时复制------不用在意

父子进程共享:

  • 文件描述符(打开文件的结构体)

  • mmap映射区(进程间通信)。

2.7 父子进程gdb调试

设置父进程调试路径

set follow-fork-mode parent (默认)

设置子进程调试路径

set follow-fork-mode child

note: 设置必须在fork函数调用之前才有效。

3- exec 函数族


当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。但进程ID不变,换核不换壳。

使进程执行某一程序。成功无返回值,失败返回-1

3.1 execlp函数

借助 PATH 环境变量加载一个进程。一般用于调用系统程序。如:lscat等。

3.1.1 函数原型
int execlp(const char *file, const char *arg, ...);	

参数从前到后为:文件名,argv[0],argv[1],argv[2]......

因此第二个参数不能写运行程序的第一个参数。

有多个参数,函数结尾必须添加NULL作为哨兵,表示参数结束。

3.1.2 示例

通过execlp让子进程去执行ls命令

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
​
int main(int argc, char *argv[])
{
​
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork error");
        exit(1);
    }
​
​
    else if(pid == 0)       // 子进程
    {
        execlp("ls","ls","-l","-h",NULL);   // 参数为:文件名,argv[0],argv[1],argv[2]......
        perror("exec error");
        exit(1);
    }
​
​
    else if(pid > 0)        // 父进程
    {
        sleep(1);       // 防止bash抢占
        printf("im parent");
    }
​
    printf("=======================\n");
​
    return 0;

3.2 execlp 函数

使用execl来让子程序调用自定义的程序

3.2.1 函数原型
int execl(const char *path, const char *arg, ...);  

execlp不同的是,第一个参数是路径,不是文件名。

这个路径用相对路径和绝对路径都行。

3.2.2 示例

使用execl来让子程序调用test.out程序。

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
​
int main(int argc, char *argv[])
{
​
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork error");
        exit(1);
    }
​
​
    else if(pid == 0)       // 子进程
    {
        execl("./test.out","./a.out",NULL);   // 参数为:文件名,argv[0],argv[1],argv[2]......
        
        // execl("/bin/ls","ls","-l",NULL);   // 也可以调用系统命令,加上路径就可以
        perror("exec error");
        exit(1);
    }
​
​
    else if(pid > 0)        // 父进程
    {
        sleep(1);       // 防止bash抢占
        printf("im parent");
    }
​
    printf("=======================\n");
​
    return 0;
}

3.3 exec函数族特性

以下示例是:

写一个程序,使用execlp执行进程查看,并将结果输出到文件里。

要用到open, execlp,dup2

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>

int main(int argc, char *argv[])
{
    int fd = open("ps.out", O_WRONLY|O_CREAT|O_TRUNC, 0644);
    if(fd < 0)
    {
        perror("open ps.out error");
        exit(1);
    }

    dup2(fd, STDOUT_FILENO);

    execlp("ps","ps","aux",NULL);
    perror("execlp error");

    // close(fd);   // 不会执行

    return 0;
}

exec函数族一般规律:

  • exec函数一旦调用成功,即执行新的程序,不返回。

  • 只有失败才返回,错误值-1,所以通常我们直接在exec函数调用后直接调用perror(),和exit()`,无需if判断。

l(list)

命令行参数列表

p(path)

搜索file时使用path变量

v(vector)

使用命令行参数数组

e(environment)

使用环境变量数组,不适用进程原有的环境变量,设置新加载程序运行的环境变量

事实上,只有execve是真正的系统调用,其他5个函数最终都调用execve,是库函数,所以execveman手册第二节,其它函数在man手册第3节。

4- 进程回收


4.1 孤儿进程和僵尸进程

孤儿进程:

  • 父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。

僵尸进程:

  • 子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。 kill 对其无效。这里要注意,每个进程结束后都必然会经历僵尸态,时间长短的差别而已。

  • 子进程终止时,子进程残留资源 PCB 存放于内核中,PCB 记录了进程结束原因,进程回收就是回收 PCB 。回收僵尸进程,得kill它的父进程,让孤儿院去回收它。

4.2 wait 回收子进程

4.2.1 wait 函数原型

回收子进程退出资源, 阻塞回收任意一个。

pid_t wait(int *status);

参数:(传出) 回收进程的状态。

返回值:

成功: 回收进程的 pid

失败: -1,errno

4.2.2 函数作用
  • 阻塞等待子进程退出

  • 清理子进程残留在内核的 pcb 资源

  • 通过传出参数,得到子进程结束状态

4.2.3 获取子进程退出值和异常终止信号

  • 获取子进程正常终止值:

WIFEXITED(status) --> 为真 -->调用 WEXITSTATUS(status) --> 得到 子进程 退出值。

  • 获取导致子进程异常终止信号:

WIFSIGNALED(status) --> 为真 -->调用WTERMSIG(status) --> 得到 导致子进程异常终止的信号编号。

4.2.4 示例

捕获程序异常终止的信号并打印

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
​
int main(void)  
{  
    pid_t pid, wpid;
    int status;
    pid = fork();
​
​
    if(pid == 0)
    {
        printf("---child, my id= %d, going to sleep 4s\n", getpid());  
        sleep(4);  
        printf("-------------child die--------------\n");  
        return 73;      // 子进程退出值
    }
    else if(pid > 0)
    {
        wpid = wait(&status);       // 如果子进程未终止,父进程阻塞在这个函数上
        if(wpid == -1)
        {
            perror("wait error");
            exit(1);
        }
​
        if(WIFEXITED(status))       // 为真,说明子进程正常终止.   
        {
            printf("child exit with %d\n", WEXITSTATUS(status));        // 打印退出值
        }
        if (WIFSIGNALED(status))    //为真,说明子进程是被信号终止. 
        {       
​
            printf("child kill with signal %d\n", WTERMSIG(status));    // 导致子进程异常终止的信号编号
        }  
    }
​
    else {  
        perror("fork");  
        return 1;  
    }  
  
    return 0;  
}  

4.3 waitpid回收子进程

4.3.1 waitpid函数原型
pid_t waitpid(pid_t pid, int *status, int options);

指定某一个进程进行回收。可以设置非阻塞。

waitpid(-1, &status, 0) == wait(&status);

参数

  • pid:指定回收某一个子进程 pid

    • > 0: 回收指定ID的子进程 pid

    • -1:回收任意子进程

    • 0:回收和当前调用waitpid一个组的所有子进程

    • < -1: 回收指定进程组内的任意子进程

  • status:(传出) 回收进程的状态。

  • optionsWNOHANG 指定回收方式为,非阻塞。

返回值

  • > 0 : 表成功回收的子进程 pid

  • 0 : 函数调用时, 参数3 指定了WNOHANG, 并且,没有子进程结束。

  • -1: 失败。errno

4.3.2 示例

回收指定子进程。

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
​
int main(int argc, char *argv[])
{
    int i;
    pid_t pid, wpid,tmpid;
​
​
    for(i = 0; i<5; i++)
    {
        pid = fork();
        if(pid == 0)    // 子进程不fork()
        {
            break;
        }
​
        if(i == 2)
        {
            tmpid = pid;
            printf("pid = %d\n", tmpid);
        }
    }
​
​
    if(5 == i)      // 父进程, 从 表达式 2 跳出  
    {
        sleep(5);
        printf("i am parent , before waitpid, pid = %d \n", tmpid);  
​
        wpid = waitpid(tmpid, NULL, WNOHANG);       // 指定一个进程回收,不阻塞
        // wpid = waitpid(tmpid, NULL, 0);          // 指定一个进程回收,阻塞
​
        if (wpid > 0) 
        {  
            printf("child  pid = %d has finished\n", wpid); 
        }
        else if(wpid == 0)
        {
            printf("child  pid = %d not finished\n", wpid); 
        }
        else if(wpid == -1)
        {
            perror("waitpid error");
            exit(1);
        }
    }
​
    else         // 子进程, 从 break 跳出  
    {
        sleep(i);
        printf("I am %dth child, pid= %d\n", i+1, getpid()); 
    }
​
    return 0;
}

4.4 waitpid 回收多个子进程

一次wait/waitpid函数调用,只能回收一个子进程。

下面的示例展示了循环回收多个子进程。

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
​
// 回收多个子进程  
int main(int argc, char *argv[])  
{  
   int i;  
   pid_t pid, wpid;  
 
   for (i = 0; i < 5; i++) {         
       pid = fork();  
        if (pid == 0) {       // 循环期间, 子进程不 fork   
            break;  
        }  
    }  
  
    if (5 == i) {       // 父进程, 从 表达式 2 跳出  
        /* 
        while ((wpid = waitpid(-1, NULL, 0))) {     // 使用阻塞方式回收子进程 
            printf("wait child %d \n", wpid); 
        } 
        */  
        while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {     //使用非阻塞方式,回收子进程.  
            if (wpid > 0) {  
                printf("wait child %d \n", wpid);  
            } else if (wpid == 0) {  
                sleep(1);  
                continue;  
            }  
        }  
  
    } else {            // 子进程, 从 break 跳出  
        sleep(i);  
        printf("I'm %dth child, pid= %d\n", i+1, getpid());  
    }  
   
    return 0;  
}  

4.5 waitwaitpid 总结

总结:

waitwaitpid 一次调用,回收一个子进程。

想回收多个用while

5- 进程间通信


5.1 进程间通信的方式

IPC进程间通信的常用方式:

  • 管道:使用简单

  • 信号:开销最小

  • mmap映射:可以用在非血缘关系进程间

  • socket ( 本地套接字 ):稳定性最高,但是实现最麻烦

5.2 管道通信

5.2.1 原理与特性

原理:内核借助环形队列机制,使用内核缓冲区实现。

特性:

1. 伪文件

2. 管道中的数据只能一次读取

3. 数据在管道中,只能单向流动

局限性:

1. 数据不能自己写,自己读

2. 数据不可以反复读

3. 双向半双工通信(数据可以双向流动,但是不能同时两个方向流动)

4. 只有血缘关系间的进程之间可以使用

5.2.2 管道使用方法
  1. pipe函数

    作用:创建并打开管道

  2. 函数原型:

    int pipe(int fd[2]);

    参数:

    • fd[0]: 读端。

    • fd[1]: 写端。

    返回值:

    • 成功:0

    • 失败:-1errno

  3. 图示

pipe.png

  1. 示例

父进程往管道里写,子进程从管道读,然后打印读取的内容

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
​
void sys_err(const char *str)
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    int ret, ret_read, ret_write;
    int fd[2];
    pid_t pid;
​
    char *str = "hello pipe()\n";
    char buf[1024];
​
    ret = pipe(fd);
    if(ret == -1)
    {
        sys_err("pipe error");
    }
​
    pid = fork();
    if(pid > 0)
    {
        close(fd[0]);   // 父进程关闭读端
        sleep(3);
        ret_write = write(fd[1],str,strlen(str));
        printf("parent write ret = %d \n", ret_write);
        sleep(1);
        close(fd[1]);
    }
​
    else if(pid == 0)
    {
        close(fd[1]);   // 子进程关闭写端
        ret_read = read(fd[0], buf, sizeof(buf));
        printf("child read ret = %d \n", ret_read);
        write(STDOUT_FILENO, buf, ret_read);
        close(fd[0]);
    }
​
    return 0;
}

5.2.3 管道读写行为

读管道:

  • 管道有数据,read返回实际读到的字节数。

  • 管道无数据:

    • 无写端,read返回0 (类似读到文件尾)

    • 有写端,read阻塞等待。

写管道:

  • 无读端, 异常终止。 (SIGPIPE导致的)

  • 有读端:

    • 管道已满, 阻塞等待

    • 管道未满, 返回写出的字节个数。

5.2.4 父子间进程通信示例

使用管道实现父子进程间 ls | wc -l 通信

  • ls命令是用于列出目录中的文件和子目录的命令

  • wc -l命令用于统计输入信息的行数

要求

  • 假定父进程实现wc,子进程实现ls

  • ls命令正常会将结果集写到stdout,但现在会写入管道写端

  • wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。

  • 要用到 pipe dup2 exec

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
​
void sys_err(const char *str)
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    int ret;
    int fd[2];
    pid_t pid;
    char buf[1024];
​
    ret = pipe(fd);
    if(ret == -1)
    {
        sys_err("pipe error");
    }
​
    pid = fork();
    if(pid == -1) 
    {
        sys_err("fork error");
    }
​
    else if(pid > 0)     // 父进程
    {
        close(fd[1]);   // 父进程关闭写端
        dup2(fd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
        sleep(1);
        close(fd[0]);
    }
​
    else if (pid == 0)   // 子进程
    {
        close(fd[0]);   // 子进程关闭读端
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls","ls",NULL);
        close(fd[1]);
    }
​
    return 0;
}

5.2.5 兄弟进程间通信

示例:

要求使用 “循环创建N个子进程的模型” 来创建兄弟进程,使用循环因子i标识,注意管道读写行为

  • 兄:ls

  • 弟:wc -l

  • 父:等待回收子进程;

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
​
void sys_err(const char *str)
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    int ret, i;
    int fd[2];
    pid_t pid;
    char buf[1024];
​
    ret = pipe(fd);
     if(ret == -1)
    {
        sys_err("pipe error");
    }
​
    for (i = 0; i < 2; i++)
    {
        if ((pid = fork()) == 0)    // 判断是否为子进程,子进程不fork
        {
            break;
        }
        if(pid == -1) 
        {
            sys_err("fork error");
        }
​
        if(pid == 0)    // 子进程出口
        {
            break;
        }
    }
    
    if(i == 2)     // 父进程
    {
        close(fd[1]);   // 父进程关闭写端
        close(fd[0]);   // 父进程关闭读端
​
        wait(NULL);
        wait(NULL);
        printf("children all finished! \n");
    }
​
    if (i == 0)   // 兄进程
    {
        close(fd[0]);   // 兄进程关闭读端
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls","ls",NULL);
        close(fd[1]);
    }
​
    if (i == 1)   // 弟进程
    {
        close(fd[1]);   // 弟进程关闭写端
        dup2(fd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
        sleep(1);
        close(fd[0]);
    }
​
    return 0;
}

5.2.6 多个读写端操作管道和管道缓冲区大小

一个读端多个写端,需要调控写入顺序,保证同一时间只有一个写端向管道内写数据,同时调整读端的时间,避免漏掉数据。

管道缓冲区大小默认4096(4K)

pipe_size.jpg

5.3 FIFO命名管道

可以用于无血缘关系的进程间通信。

fifo操作起来像文件

5.3.1 FIFO管道使用方法
  1. mkfifo函数

    作用:创建并打开管道

  2. 函数原型:

    int mkfifo(const char *pathname, mode_t mode);

    参数:

    • pathname: 要创建的命名管道的路径名

    • mode: 创建的命名管道的权限模式。通常使用八进制表示的权限值,例如 0666

    • open fifo O_RDONLY: 读端。

    • open fifo O_WRONLY: 写端。

    返回值:

    • 成功:0

    • 失败:-1errno

  3. 示例

    非血缘关系进程,一个写fifo,一个读fifo,操作起来就像文件一样的

    写端:

    #include <stdio.h>  
    #include <stdlib.h>  
    #include <fcntl.h>
    #include <unistd.h>  
    #include <string.h>  
    #include <pthread.h>
    #include <sys/stat.h>
    #include <sys/wait.h>
    ​
    #define FIFO_PATH "myfifo"
    ​
    void sys_err(const char *str)
    {
        perror(str);
        exit(1);
    }
    ​
    int main(int argc, char *argv[])
    {
        int fd, i = 0;
        char buf[4096];
    ​
        mkfifo(FIFO_PATH, 0666);
        fd = open(FIFO_PATH, O_WRONLY);
    ​
        if(fd == -1)
        {
            sys_err("open error");
        }
        
        // 写数据
        while(1)
        {
            sprintf(buf,"hello myfifo %d \n", i++);
    ​
            write(fd, buf, strlen(buf));    // 向管道写数据
            sleep(1);
        }
        close(fd);
    ​
        // 删除命名管道
        unlink(FIFO_PATH);
    ​
        return 0;
    }

    读端:

    #include <stdio.h>  
    #include <stdlib.h>  
    #include <fcntl.h>
    #include <unistd.h>  
    #include <string.h>  
    #include <pthread.h>
    #include <sys/stat.h>
    #include <sys/wait.h>
    ​
    #define FIFO_PATH "myfifo"
    ​
    void sys_err(const char *str)
    {
        perror(str);
        exit(1);
    }
    ​
    int main(int argc, char *argv[])
    {
        // 打开管道进行读取
        int fd, len;
        char buf[4096];
    ​
        fd = open(FIFO_PATH, O_RDONLY);
        if(fd == -1)
        {
            sys_err("open error");
        }
    ​
        // 读取数据
        while(1)
        {
            len = read(fd, buf, sizeof(buf));   // 从管道读数据
            write(STDOUT_FILENO, buf, len);
            sleep(1);   // 多个读端时应该增加睡眠秒数,放大效果
        }
    ​
        close(fd);
    ​
        return 0;
    }

    一个读端,多个写端或者一个写端多个读端都是可以的,在一个写端多个读端时,数据一旦被读走就没了,就是数据不可重复读取。所以多个读端的并集才是写端的写入数据。

5.3.2 文件实现进程间通信

文件通信,有没有血缘关系都行

  • 只是有血缘关系的进程对于同一个文件,使用的同一个文件描述符,

  • 没有血缘关系的进程,对同一个文件使用的文件描述符可能不同。

  • 这些都不是问题,打开的是同一个文件就行。

5.4 存储映射I/O

5.4.1 mmap函数

创建共享内存映射

  1. 函数原型:

    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

    参数:

    • addr: 指定映射区的首地址。通常传【NULL】,表示让系统自动分配

    • length:共享内存映射区的大小。(【<= 】文件的实际大小)

    • prot: 共享内存映射区的读写属性。

      • PROT_READPROT_WRITEPROT_READ|PROT_WRITEPROT_EXEC

    • flags: 标注共享内存的共享属性。

      • MAP_SHARED 修改会反映到磁盘上

      • MAP_PRIVATE 修改不反映到磁盘上

      • MAP_ANONYMOUS 匿名映射,不用手动依赖文件

  • fd: 用于创建共享内存映射区的那个文件的 文件描述符。

  • offset:默认0,表示映射文件全部。偏移位置。需是 【4k 的整数倍】。

返回值:

  • 成功:映射区的首地址。

  • 失败:MAP_FAILED (void*(-1))errno

5.4.2 munmap函数

释放映射区。

  1. 函数原型

    #include <sys/mman.h>
    ​
    int munmap(void *addr, size_t length);  

    参数:

    • addrmmap 的返回值

    • length:大小

5.4.3 mmap示例

使用mmap创建一个映射区(共享内存),并往映射区里写入内容并打印出来。

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
#include <sys/mman.h>   // 需要引入头文件
​
void sys_err(const char *str)       // 错误处理函数
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    // 打开文件
    int fd = open("testmap", O_RDWR|O_CREAT|O_TRUNC, 0644);
    if(fd == -1)
    {
        sys_err("open error");
    }
​
    // 扩展文件大小
    /*
    lseek(fd, 20, SEEK_END);        // 这两个函数等价于ftruncate()函数
    write(fd, "\0", 1);
    */
    ftruncate(fd, 20);
    int len = lseek(fd, 0, SEEK_END);
​
    // 映射内存
    char *p = NULL;
    p = mmap(NULL, len,PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED)
    {
        sys_err("mmap error");
    }
​
    // 使用 p 对文件进行读写操作
    strcpy(p, "hello mmap");        // 写操作
    printf("-----%s \n", p);        // 读操作
​
    // 释放映射区
    int ret = munmap(p, len);
    if(ret == -1)
    {
        sys_err("mnmap error");
    }
​
    return 0;
}

5.4.4 mmap使用注意事项
  • 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。

  • 用于创建映射区的文件大小为 0,实际指定0大小创建映射区, 出 “无效参数”错误。

  • 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”错误。

  • 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该 <= 文件的open权限。只写不行,至少需要读。

  • 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用地址访问。

  • offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )

  • 对申请的映射区内存,不能越界访问。

  • munmap用于释放的 地址,必须是mmap申请返回的地址。

  • 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。

  • 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。

  • mmap函数的保险调用方式:

    fd = open("文件名", O_RDWR);
    mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

5.4.5 有血缘父子进程间的mmap通信

要求:对比全局变量

  • 父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );

  • 指定 MAP_SHARED 权限

  • fork() 创建子进程。

  • 一个进程读, 另外一个进程写。

#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
#include <sys/mman.h>   // 需要引入头文件
​
int var = 100;
​
void sys_err(const char *str)       // 错误处理函数
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    int *p;
    pid_t pid;
    
    // open file
    int fd = open("temp", O_RDWR|O_CREAT|O_TRUNC,0644);
    if(fd == -1)
    {
        sys_err("open error");
    }
    ftruncate(fd, 4);
​
    // mmap
    // p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);       // shared 
    p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);     // private
    if(p == MAP_FAILED)
    {
        sys_err("mmap error");
    }
​
    // close fd
    close(fd);
​
    // creat fork()
    pid = fork();
    if(pid == 0)        // son
    {
        *p = 7000;
        var = 1000;
        printf("child, *p = %d, var = %d \n", *p, var);
    }
    if(pid > 0)         // father
    {
        sleep(1);
        printf("parent, *p = %d, var = %d \n", *p, var);
        wait(NULL);
​
        int ret = munmap(p, 4);
        if(ret == -1)
        {
            sys_err("munmap error");
        }
    }
​
    return 0;
}

5.4.6 无血缘父子进程间的mmap通信

写端

// WRITE
​
#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
#include <sys/mman.h>   // 需要引入头文件
​
struct student
{
    int id;
    char name[256];
    int age;
};
​
​
​
​
void sys_err(const char *str)       // 错误处理函数
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    struct student stu = {1, "xiaoming", 18};
    struct student *p;
​
    // open file
    int fd = open("temp", O_RDWR|O_CREAT|O_TRUNC,0644);
    if(fd == -1)
    {
        sys_err("open error");
    }
    ftruncate(fd, sizeof(stu));
​
    // mmap
    p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED)
    {
        sys_err("mmap error:");
    }
​
    // close fd
    close(fd);
​
    // write
    while(1)
    {
        memcpy(p, &stu, sizeof(stu));
        stu.id++;
        stu.age++;
        sleep(1);
    }
​
    munmap(p, sizeof(stu));
    
​
    return 0;
}

读端

// READ
​
#include <stdio.h>  
#include <stdlib.h>  
#include <fcntl.h>
#include <unistd.h>  
#include <string.h>  
#include <pthread.h>
#include <sys/wait.h>
#include <sys/mman.h>   // 需要引入头文件
​
struct student
{
    int id;
    char name[256];
    int age;
};
​
​
​
​
void sys_err(const char *str)       // 错误处理函数
{
    perror(str);
    exit(1);
}
​
int main(int argc, char* argv[])
{
    struct student stu;
    struct student *p;
​
    // open file
    int fd = open("temp", O_RDONLY);
    if(fd == -1)
    {
        sys_err("open error");
    }
​
    // mmap
    p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED)
    {
        sys_err("mmap error:");
    }
​
    // close fd
    close(fd);
​
    // read
    while(1)
    {
        printf("id = %d, name = %s, age = %d \n", p->id, p->name, p->age);
        sleep(1);
    }
​
    munmap(p, sizeof(stu));
    
​
    return 0;
}

无血缘关系进程间通信。

  • mmap

  • 数据可以重复读取。内容被读走之后不会消失,

    • 如果读进程的读取时间间隔短,它会读到很多重复内容,因为写进程没来得及写入新内容。

  • fifo

  • 数据只能一次读取。

5.4.7 mmap匿名映射区

无须依赖于一个文件即可创建映射区。只能用于血缘关系进程间通信。

代码:

使用MAP_ANONYMOUS 或者 MAP_ANON宏。

p = (int *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);