进程间通信、文件描述符

进程间通信功能

进程间通信功能
1、数据传输:一个进程需要将它的数据发送给另一个进程
2、资源共享:多个进程之间共享同样的资源
3、通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件
4、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变

Linux操作系统支持的主要进程间通信的通信机制
在这里插入图片描述

信号图表

信号图表
在这里插入图片描述
在这里插入图片描述

kill函数

kill函数

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <sys/wait.h>

#include <sys/types.h>
#include <signal.h>

int main()
{

    pid_t pid;
    pid = fork();
    if(pid <0)
    {
        perror("fail to fork");
        exit(1);
    }
    else if(pid > 0)  // 父进程的代码区
    {
        while (1)
        {
            printf("this is a father process\n");
            sleep(1);
        }
    }
    else if(pid == 0) // 子进程的代码区
    {
        printf("this is a son process\n");

        // 子进程在3秒之后,让父进程退出
        sleep(3);
        // 使用kill给父进程发送信号,然后父进程接收到信号后直接退出
        kill(getppid(), SIGINT);
    }


    return 0;
}


alarm函数

alarm函数
在这里插入图片描述

#include <stdio.h>
#include<unistd.h>

int main()
{
    unsigned int sec;
    // 当执行到alarm之后,代码会接着往下执行,当设定的时间到后,会产生SIGALRM信号


    /*
     如果alarm之前没有设置其他闹钟,则返回0。如果之前设置了,则返回之前剩余的秒数
     如果一个程序中出现多个alarm闹钟,第一个如果没有到达指定的时间就遇到第二个,则第一个的时间清除,按照第二个alarm的时间继续向下运行
     */

    sec = alarm(5);
    printf("sec = %d\n", sec);

    sleep(3);

    sec = alarm(6);
    printf("sec = %d\n", sec);

    while (1)
    {
        printf("hello world\n");
        sleep(1);
    }

    return 0;
}


在这里插入图片描述

raise函数

raise函数
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include<signal.h>

#include<unistd.h>
#include<sys/types.h>

int main()
{

    int num = 0;

    while (1)
    {
        printf("hello world\n");
        sleep(1);
        num++;

        if(num == 5)
        {
            // 使用raise给当前进程本身发送信号
            raise(SIGALRM);

            // kill(getpid(),SIGALRM);
        }
    }

    return 0;
}


abort函数

abort函数
在这里插入图片描述

pause函数


signal函数

signal函数

进程接收到信号后的处理方式
1、执行系统默认动作
2、忽略此信号
3、执行自定义信号处理函数
程序中可用函数signal()改变信号的处理方式

在这里插入图片描述
功能: 注册信号处理函数(不可用于SIGKILL、SIGSTOP信号),即确定收到信号后处理函数的入口地址。

参数:

  • signum:信号编号
  • handler的取值
    • 忽略该信号:SIG_IGN
    • 执行系统默认动作:SIG_DFL
    • 自定义信号处理函数:信号处理函数名
  • 返回值
    • 成功:返回函数地址,该地址为此信号上一次注册的信号处理函数的地址
    • 失败:返回SIG_ERR

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

#include <stdio.h>
#include <stdlib.h>
#include<signal.h>
#include<unistd.h>

void handler(int sig);

int main()
{
    // 以默认的方式处理信号
#if 0

    if(signal(SIGINT, SIG_DFL) == SIG_ERR)  // SIGINT 键盘输入ctrl+c时产生信号
    {
        perror("fail to signal");
        exit(1);
    }
    if(signal(SIGQUIT, SIG_DFL) == SIG_ERR)  // SIGQUIT 键盘输入ctrl+\时产生信号
    {
        perror("fail to signal");
        exit(1);
    }
    if(signal(SIGTSTP, SIG_DFL) == SIG_ERR)   //  SIGTSTP 键盘输入ctrl+z时产生信号
    {
        perror("fail to signal");
        exit(1);
    }

#endif

    // 以忽略的方式来处理信号
#if 0

    if(signal(SIGINT, SIG_IGN) == SIG_ERR)  // SIGINT 键盘输入ctrl+c时产生信号
    {
        perror("fail to signal");
        exit(1);
    }
    if(signal(SIGQUIT, SIG_IGN) == SIG_ERR)  // SIGQUIT 键盘输入ctrl+\时产生信号
    {
        perror("fail to signal");
        exit(1);
    }
    if(signal(SIGTSTP, SIG_IGN) == SIG_ERR)   //  SIGTSTP 键盘输入ctrl+z时产生信号
    {
        perror("fail to signal");
        exit(1);
    }

    // SIGKILL 只能以默认的方式处理,不能忽略或捕捉   会报错:fail to signal: Invalid argument
    /*
    if(signal(SIGKILL, SIG_IGN) == SIG_ERR)
    {
        perror("fail to signal");
        exit(1);
    }
    */


#endif

    // 以用户自定义的方式来处理信号
#if 1
    if(signal(SIGINT, handler) == SIG_ERR)  // SIGINT 键盘输入ctrl+c时产生信号
    {
        perror("fail to signal");
        exit(1);
    }
    if(signal(SIGQUIT, handler) == SIG_ERR)  // SIGQUIT 键盘输入ctrl+\时产生信号
    {
        perror("fail to signal");
        exit(1);
    }
    if(signal(SIGTSTP, handler) == SIG_ERR)   //  SIGTSTP 键盘输入ctrl+z时产生信号
    {
        perror("fail to signal");
        exit(1);
    }

#endif


    while (1)
    {
        printf("hello world\n");
        sleep(1);
    }

    return 0;

}

void handler(int sig)
{
    if(sig == SIGINT)
    {
        printf("SIGINT 正在处理\n");
    }
    if(sig == SIGQUIT)
    {
        printf("SIGQUIT 正在处理\n");
    }
    if(sig == SIGTSTP)
    {
        printf("SIGTSTP 正在处理\n");
    }
}




signal函数的返回值
举例:

#include <stdio.h>
#include <stdlib.h>
#include<signal.h>
#include<unistd.h>

void *ret_handler;

void handler(int sig)
{
    printf("cccccccccctestcccccccccc\n");

    if(signal(SIGINT, ret_handler) == SIG_ERR)
    {
        perror("fail to signal");
        exit(1);
    }
}

int main()
{
    if( (ret_handler = signal(SIGINT, handler)) == SIG_ERR )
    {
        perror("fail to signal");
        exit(1);
    }

    while (1)
    {
        printf("hello world\n");
        sleep(1);
    }
    return 0;
}


执行结果:
在这里插入图片描述

可重入函数

可重入函数

是指函数可以由多个任务并发使用,而不必担心数据错误

可重入函数就是可以被中断的函数,当前函数可以在任何时刻中断它,并执行另一块代码。当执行完毕后,回到原本的代码还可以正常继续运行

#include <stdio.h>
#include <stdlib.h>
#include<signal.h>
#include<unistd.h>

void handler(int sig)
{
    printf("cccccccccctestcccccccccc\n");

}

int main()
{
    if(signal(SIGINT, handler) == SIG_ERR)
    {
        perror("fail to signal");
        exit(1);
    }

#if 0
    // sleep是一个可重入函数,但是当执行信号处理函数之后,不会回到原本的位置继续睡眠
    // sleep(10);

    // alarm函数是一个可重入函数,当它执行时,如果有信号产生并执行信号处理函数,执行完毕后,会继续运行。
    alarm(10);

    while (1)
    {
        printf("hello world\n");
        sleep(1);
    }
#endif

#if 1

    char buf[32] ="";
    // read 也是一个可重入函数,在等待终端输入时,如果产生信号并执行信号处理函数,信号处理函数执行完毕后,可以继续输入数据,read可以读取到信号处理函数之后的数据
    if(read(0, buf, 20) == -1)
    {
        perror("fail to read");
        exit(1);
    }

    printf("buf = [%s]\n", buf);
#endif

    return 0;
}



信号集


信号集

#include <stdio.h>
#include <stdlib.h>
#include<signal.h>
#include<unistd.h>



int main()
{

    // 创建一个信号集
    sigset_t set;
    int ret = 0;

    // 初始化一个空的信号集
    sigemptyset(&set);

    // 判断SIGINT信号是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0)
    {
        printf("SIGINT is not a member of sigprocmask \nret = %d\n", ret);
    }

    // 将指定的信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    ret = sigismember(&set, SIGINT);
    if(ret == 1)
    {
        printf("SIGINT is a member of sigprocmask \nret = %d\n", ret);
    }

    return 0;
}



在这里插入图片描述

信号阻塞集

信号阻塞集(屏蔽集、掩码)

1、每个进程都有一个阻塞集,它用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。
2、所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  • 功能:检查或修改信号阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由set指定,而原先的信号阻塞集由oldset保存。
  • 参数:
    • how:信号阻塞集合的修改方法
    • set:要操作的信号集地址
    • oldset:保存原先信号集地址
  • how:
    • SIG_BLOCK:向信号阻塞集合中添加set信号集
    • SIG_UNBLOCK:从信号阻塞集合中删除set集合
    • SIG_SETMASK:将信号阻塞集合设为set集合
      注:若set为NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中。
#include <stdio.h>
#include <stdlib.h>
#include<signal.h>
#include<unistd.h>

int main()
{

    // 创建一个信号集
    sigset_t set;
    int i = 0;

    // 初始化一个空的信号集
    sigemptyset(&set);
    // 在信号集中添加信号
    sigaddset(&set, SIGINT);

    while (1)
    {
        // 将set信号集添加到信号阻塞集中
        sigprocmask(SIG_BLOCK, &set, NULL);
        for(i=0; i<5; i++)
        {
            printf("SIGINT signal is blocked\n");
            sleep(1);
        }

        // 将set信号集从信号阻塞集中删除
        sigprocmask(SIG_UNBLOCK, &set, NULL);
        for(i=0; i<5; i++)
        {
            printf("SIGINT signal unblocked\n");
            sleep(1);
        }
    }

    return 0;
}



管道

管道:又叫无名管道。

无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

任何一个进程在创建的时候,系统都会给他分配4G的虚拟内存,分为3G的用户空间和1G的内核空间。内核空间是所有进程公有的,无名管道就是创建在内核空间的,多个进程知道同一个无名管道的空间,就可以利用它来进行通信。

在这里插入图片描述
无名管道虽然是在内核空间创建的,但是会给当前用户进程两个文件描述符,一个负责执行读操作,一个负责执行写操作

管道是最古老的UNIX IPC方式,其特点是:

1、半双工,数据在同一时刻只能在一个方向上流动
2、数据只能从管道的一端写入,从另一端读出
3、写入管道中的数据遵循先入先出的规则
4、管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等
5、管道不是普通的文件,不属于某个文件系统,其只存在于内存中
6、管道在内存中对应一个缓冲区。不同的系统其大小不一定相同
7、从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据
8、管道没有名字,只能在具有公共祖先的进程之间使用

无名管道的创建pipe函数

无名管道的创建pipe函数

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

int main()
{
    // 使用pipe创建一个无名管道
    int fd_pipe[2];
    if(pipe(fd_pipe) == -1)
    {
        perror("fail to pipe");
        exit(1);
    }

    printf("fd_pipe[0] = %d\n", fd_pipe[0]);
    printf("fd_pipe[1] = %d\n", fd_pipe[1]);

    // 对无名管道执行读写操作
    // 无名管道给当前用户进程两个文件描述符,所以只要操作这两个文件描述符
    // 通过文件IO中的read和write函数对无名管道进行操作

    // 如果管道中有数据,再次写入的数据会放在之前数据的后面,不会把之前的数据替换
    // 读取数据时,直接从管道中读取指定个数的数据,如果管道中没有数据了,则read函数会阻塞等待


    // 通过write函数向无名管道中写入数据
    if(write(fd_pipe[1], "hello world", 11) == -1)
    {
        perror("fail  to  write");
        exit(1);
    }

    write(fd_pipe[1], "nihao beijing",strlen("nihao beijing")+1);

    //通过read函数从无名管道中读取数据
    char buf[32] = "";
    // if(read(fd_pipe[0], buf, sizeof(buf)) == -1)

    ssize_t bytes;
    if((bytes = read(fd_pipe[0], buf, 20)) == -1)
    {
        perror("fail to read");
        exit(1);
    }
    printf("buf = [%s]\n", buf);
    printf("bytes = %ld\n", bytes);


    bytes = (read(fd_pipe[0], buf, sizeof(buf)));
    printf("buf = [%s]\n", buf);
    printf("bytes = %ld\n", bytes);

    bytes = (read(fd_pipe[0], buf, sizeof(buf)));
    printf("buf = [%s]\n", buf);
    printf("bytes = %ld\n", bytes);
    return 0;
}



在这里插入图片描述

无名管道实现进程间通信

无名管道实现进程间通信

利用无名管道实现进程间的通信,都是父进程创建无名管道,然后再创建子进程,子进程继承父进程的无名管道的文件描述符,然后父子进程通过读写无名管道实现通信

// 使用无名管道实现父子进程间的通信
// 由于无名管道创建之后给当前进程两个文件描述符,所以如果是完全不相关的进程,无法获取同一个无名管道的文件描述符
// 所以无名管道只能在具有亲缘关系的进程间通信

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

int main()
{


    // 使用无名管道实现父子进程间的通信
    // 由于无名管道创建之后给当前进程两个文件描述符,所以如果是完全不相关的进程,无法获取同一个无名管道的文件描述符
    // 所以无名管道只能在具有亲缘关系的进程间通信

    pid_t pid;
    int pipefd[2];

    if(pipe(pipefd) == -1)
    {
        perror("fail to pipe");
        exit(1);
    }

    if((pid = fork()) <0 )
    {
        perror("fail to fork");
        exit(1);
    }
    else if(pid >0)  // 父进程
    {
        // 父进程负责给子进程发送数据
        char buf[128] = {};
        while (1)
        {
            fgets(buf,sizeof(buf), stdin);  // 从终端读取数据
            buf[strlen(buf) - 1] = '\0';    // 把string最后面的\n修改成\0

            if(write(pipefd[1], buf, sizeof(buf)) == -1)
            {
                perror("fail to write");
                exit(1);
            }
        }
    }
    else // pid == 0 子进程
    {
        // 子进程接收父进程的数据
        char buf[128] = "";
        while (1)
        {
            if(read(pipefd[0], buf, sizeof(buf)) == -1)
            {
                perror("fail to read");
                exit(1);
            }

            printf("from parent: %s\n", buf);
        }
    }


    return 0;
}



无名管道的读写规律

无名管道的读写规律

1、默认用read函数从管道中读数据是阻塞的
2、调用write函数向管道里写数据,当缓冲区已满时write也会阻塞
3、通信过程中,读端口全部关闭后,写进程向管道内写数据时,写进程会(收到SIGPIPE信号)退出

  • 读写端都存在,只读不写

// 如果管道中有数据,会正常读取数据
// 如果管道中没有数据,则读操作会阻塞等待,直到有数据为止

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

int main()
{
    int pipefd[2];

    if(pipe(pipefd) == -1)
    {
        perror("fail to pipe");
        exit(1);
    }

    // 读写端都存在,只读不写
    // 如果管道中有数据,会正常读取数据
    // 如果管道中没有数据,则读操作会阻塞等待,直到有数据为止

    write(pipefd[1], "hello world", 11);

    char buf[128] = "";
    if(read(pipefd[0], buf, sizeof(buf)) == -1)
    {
        perror("fail to read");
        exit(1);
    }

    printf("buf = %s\n", buf);

    if(read(pipefd[0], buf, sizeof(buf)) == -1)
    {
        perror("fail to read");
        exit(1);
    }

    printf("buf = %s\n", buf);

    return 0;
}



  • 读写端都存在,只写不读

// 如果一直执行写操作,则无名管道对应的缓冲区会被写满,写满之后,write函数也会阻塞等待
// 默认无名管道的缓冲区64K字节

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>

int main()
{
    int pipefd[2];

    if(pipe(pipefd) == -1)
    {
        perror("fail to pipe");
        exit(1);
    }

    // 读写端都存在,只写不读
    // 如果一直执行写操作,则无名管道对应的缓冲区会被写满,写满之后,write函数也会阻塞等待
    // 默认无名管道的缓冲区64K字节

    int num = 0;
    while (1)
    {
        if(write(pipefd[1], "6666", 1024) == -1)
        {
            perror("fail to write");
            exit(1);
        }
        num++;
        printf("num = %d\n", num);
    }

    return 0;
}



在这里插入图片描述

  • 只有读端

// 如果原本管道中有数据,则读操作正常读取数据
// 如果管道中没有数据,则read函数会返回0

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

int main()
{
    int pipefd[2];

    if(pipe(pipefd) == -1)
    {
        perror("fail to pipe");
        exit(1);
    }

    // 只有读端

    write(pipefd[1], "hello world", 11);

    // 关闭写文件描述符,只剩读端
    // 如果原本管道中有数据,则读操作正常读取数据
    // 如果管道中没有数据,则read函数会返回0

    close(pipefd[1]);

    char buf[128] ="";
    ssize_t bytes;

    if((bytes = read(pipefd[0], buf, sizeof(buf))) == -1 )
    {
        perror("fail to read");
        exit(1);
    }
    printf("bytes = %ld\n", bytes);
    printf("buf = %s\n", buf);

    // 清除字符串中的内容
    memset(buf, 0, sizeof(buf));

    if((bytes = read(pipefd[0], buf, sizeof(buf))) == -1 )
    {
        perror("fail to read");
        exit(1);
    }
    printf("bytes = %ld\n", bytes);
    printf("buf = %s\n", buf);

    return 0;
}



在这里插入图片描述

  • 只有写端

// 关闭读操作文件描述符,只剩写端
// 如果关闭读端,一旦执行写操作,就会产生一个信号(SIGPIPE 管道破裂),这个信号的默认处理方式就是退出进程

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>

void handler(int sig)
{
    printf("SIGPIPE信号产生了,管道破裂了\n");
}

int main()
{
    signal(SIGPIPE, handler);

    int pipefd[2];

    if(pipe(pipefd) == -1)
    {
        perror("fail to pipe");
        exit(1);
    }

    // 只有写端

    // 关闭读操作文件描述符,只剩写端
    // 如果关闭读端,一旦执行写操作,就会产生一个信号(SIGPIPE 管道破裂),这个信号的默认处理方式就是退出进程
    close(pipefd[0]);

    int num = 0;

    while (1)
    {
        if(write(pipefd[1], "hello world", 1024) == -1)
        {
            perror("fail to write");
            exit(1);
        }
        num++;
        printf("num = %d\n", num);
    }

    return 0;
}



在这里插入图片描述


通过fcntl函数设置文件的阻塞特性

通过fcntl函数设置文件的阻塞特性

  • 设置为阻塞
    fcntl(fd, F_SETFL, 0);
  • 设置为非阻塞
    fcntl(fd, F_SETFL, O_NONBLOCK);

这里的非阻塞
1、如果是阻塞,管道中没有数据,read会一直等待,知道有数据才会继续运行,否则一直等待
2、如果是非阻塞,read函数运行时,会先看一下管道中是否有数据,如果有数据,则正常运行读取数据,如果管道中没有数据,read函数会立即返回,继续下面的代码运行

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include <fcntl.h>

int main()
{
    int pipefd[2];
    char buf[] = "hello world";
    pid_t pid;

    if(pipe(pipefd) < 0)
    {
        perror("fail to pipe");
        exit(1);
    }

    pid = fork();

    if(pid < 0)
    {
        perror("fail to fork");
        exit(0);
    }
    if(pid == 0)
    {
        while (1)
        {
            sleep(5);
            write(pipefd[1], buf, strlen(buf));
        }
    }
    else
    {
        // 将pipefd[0]设置为阻塞
        // fcntl(pipefd[0], F_SETFL, 0);

        // 将pipefd[0]设置为非阻塞
        fcntl(pipefd[0], F_SETFL, O_NONBLOCK);

        while (1)
        {
            memset(buf, 0, sizeof(buf));
            read(pipefd[0], buf, sizeof(buf));
            printf("buf=[%s]\n", buf);
            sleep(1);
        }
    }

    return 0;
}



文件描述符

文件描述符

注意:Linux中一个进程最多只能打开NR_OPEN_DEFAULT(即1024)个文件,故当文件不再使用时应及时调用close函数关闭文件

文件描述符的复制

dup和dup2是两个非常有用的系统调用,都是用来复制一个文件的描述符,使新的文件描述符也标识旧的文件描述符所标识的文件

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup和dup2经常用来重定向进程的stdin、stdout和stderr

  • 案例1:使用dup函数复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>

int main()
{
    // 通过dup函数复制一个文件描述符
    int fd;
    // dup执行后给返回值文件描述符分配的值是文件描述符表中最小可用的文件描述符
    fd = dup(1);
    printf("fd = %d\n", fd);
    // 由于通过dup函数将1这个文件描述符复制了一份为fd,所以fd现在就相当于1,写数据就是向终端写入数据
    write(fd, "nihao beijing\n", strlen("nihao beijing\n"));

    return 0;
}



  • 案例2:实现输出重定向的功能
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>

int main()
{
    // 如果需要实现输出重定向的功能
    // 首先想到printf函数是操作文件描述符1所对应的文件
    // 默认是操作终端,只要能够把1对应标识的文件改变,就可以实现输出重定向
    // 所以实现创建好文件对应的文件描述符之后,将1文件描述符关闭,接着通过dup函数复制的新的文件描述符就是1
    // 这样printf函数对1操作,就写到了文件中

    int fd_file;
    fd_file = open("test.txt",O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if(fd_file == -1)
    {
        perror("fail to open");
        exit(1);
    }

    close(1);

    int fd = dup(fd_file);
    printf("hello world\n");

    printf("fd = %d\n", fd);

    return 0;
}



在这里插入图片描述

  • 案例3:实现输出重定向后,还想接着标准输出,如何实现
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>

int main()
{

    int fd1;
    int fd2;
    fd2 = dup(1);
    printf("new:fd2 = %d\n",fd2);

    fd1 = open("test.txt",O_RDWR | O_CREAT , 0664);
    close(1);

    int fd3 = dup(fd1);
    printf("hello world\n");
    printf("fd = %d\n", fd3);

    close(1);
    int fd4 = dup(fd2);
    printf("nihao beijing\n");
    printf("fd = %d\n", fd4);

    return 0;
}



dup2函数

#include <unistd.h>
int dup2(int oldfd, int newfd);

  • 功能:复制一份打开的文件描述符oldfd,并分配新的文件描述符newfd,newfd也标识lodfd所标识的文件
  • 注意:newfd是小于文件描述符最大允许值的非负整数,如果newfd是一个已经打开的文件描述符,则首先关闭该文件,然后再复制
  • 参数:
    • 要复制的文件描述符oldfd
    • 分配的新的文件描述符newfd
  • 返回值:
    • 成功:返回newfd
    • 失败:返回-1,错误代码存于errno中
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>

int main()
{

    int fd1;
    int fd2;

    fd1 = open("test.txt",O_WRONLY | O_CREAT , 0666);

    if(fd1 < 0)
    {
        perror("fail to open");
        exit(1);
    }

    // 首先关闭1文件描述符,然后将fd1复制给1,意味着1和fd1都标识test.txt文件,返回值fd2也是1
    fd2 = dup2(fd1, 1);

    printf("hello world\n");
    printf("fd2 = %d\n", fd2);

    return 0;
}



实现输出重定向后,还想接着标准输出,使用dup2如何实现

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd1;
    // 注意:如果使用dup2,则需要首先给第二个参数对应的变量赋一个初始值
    int fd2 = 3;

    // 将1复制一份为fd2,所以fd2标识的是标准输出
    dup2(1, fd2);
    printf("fd2 = %d\n", fd2);

    fd1 = open("test.txt",O_RDWR | O_CREAT , 0664);

    // 输出重定向:关闭文件描述符1,将fd1复制一份为1,所以1此时标识的是test.txt文件
    dup2(fd1, 1);
    printf("hello world\n");

    // 再次实现标准输出:关闭文件描述符1,将fd2复制一份为1,所以1此时标识的是标准输出
    dup2(fd2, 1);
    printf("你好北京\n");

    return 0;
}



命名管道

命名管道

命名管道(FIFO)和管道(pipe)基本相同,但也有一些显著的不同处。

特点:

1、半双工,数据在同一时刻只能在一个方向上流动
2、写入FIFO中的数据遵循先入先出的规则
3、FIFO所传送的数据是无格式的,这要求FIFO的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等
4、FIFO在文件系统中作为一个特殊的文件而存在并且在文件系统中可见,但FIFO中的内容却存放在内存中
5、管道在内存中对应一个缓冲区,不同的系统其大小不一定相同
6、从FIFO读数据是一次性操作,数据一旦被读,它就从FIFO中被抛弃,释放空间以便写更多的数据
7、当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用
8、FIFO有名字,不相关的进程可以通过打开命名管道进行通信

有名管道的创建

有名管道的创建

  • 方法一:

用shell命令mkfifo创建有名管道
mkfifo 文件名

  • 方法二:

使用函数mkfifo

#include <sys/types.h>
#include <sys/stat.h>

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

  • 功能:
    • 创建一个有名管道,产生一个本地文件系统可见的文件pathname
  • 参数:
    • pathname:有名管道创建后生成的文件,可以带路径
    • mode:管道文件的权限,一般通过八进制数设置即可,例如0664
  • 返回值
    • 成功:0
    • 失败:-1
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>

int main()
{
    // 通过mkfifo函数创建有名管道
    if(mkfifo("fifo_file", 0664) == -1)
    {
        // printf("errno = %d\n", errno);
        // 如果管道文件已经存在,则不需要报错退出。直接拿这个管道文件来使用即可
        // 所以需要在错误输出之前把文件存在错误排除掉
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    return 0;
}



有名管道的基本读写操作

有名管道的基本读写操作

由于有名管道在本地创建了一个管道文件,所以系统调用的IO函数基本都可以对有名管道进行操作,但是不能使用lseek修改管道文件的偏移量

注意:有名管道创建的本地的文件只是起到标识作用,真正有名管道实现进程间通信还是在内核空间开辟内存。本地产生的文件只是一个标识作用,没有其他意义,对本地管道文件的操作实质就是对内核空间的操作

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>


#define FIFONAME "fifo_file"

int main()
{
    // 通过mkfifo函数创建有名管道
    if(mkfifo(FIFONAME, 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    // 对有名管道进行操作

    // 通过open函数打开管道文件并得到文件描述符
    int fd;
    fd = open(FIFONAME, O_RDWR);
    if(fd == -1)
    {
        perror("fail to open");
        exit(1);
    }
    // 通过write函数向管道中写入数据
    if(write(fd, "hello world", strlen("hello world")) == -1)
    {
        perror("fail to write");
        exit(1);
    }

    write(fd, "nihao beijing\n", strlen("nihao beijing\n"));

    // 通过read函数读取管道中的数据
    char buf[32] = "";
    if(read(fd, buf, sizeof(buf)) == -1)
    {
        perror("fail to read");
        exit(1);
    }

    printf("buf = [%s]\n", buf);

    // 使用close函数关闭文件描述符
    close(fd);

    return 0;
}



有名管道实现进程间通信

有名管道实现进程间通信

send.c

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>


#define FIFONAME "fifo_file"
#define FIFONAME2 "fifo_file2"

int main()
{
    // 如果没有创建有名管道,则创建有名管道
    // 为了实现两个进程都可以收发数据,所以需要创建两个有名管道
    if(mkfifo(FIFONAME, 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    if(mkfifo(FIFONAME2, 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    // 打开两个有名管道并得到文件描述符
    int fd_w, fd_r;

    if((fd_w = open(FIFONAME, O_WRONLY)) == -1)
    {
        perror("fail to open");
        exit(1);
    }
    if((fd_r = open(FIFONAME2, O_RDONLY)) == -1)
    {
        perror("fail to open");
        exit(1);
    }

    char buf[128] = "";
    size_t bytes;

    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) -1] = '\0';


        // 将数据写入fifo_file,接着从fifo_file2中读取数据
        if((bytes = write(fd_w, buf, sizeof(buf))) == -1)
        {
            perror("fail to write");
            exit(1);
        }

        if((bytes = read(fd_r, buf, sizeof(buf))) == -1)
        {
            perror("fail to read");
            exit(1);
        }

        printf("from recv: %s\n", buf);

    }

    return 0;
}



recv.c

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>


#define FIFONAME "fifo_file"
#define FIFONAME2 "fifo_file2"

int main()
{

    if(mkfifo(FIFONAME, 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    if(mkfifo(FIFONAME2, 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    int fd_w, fd_r;

    if((fd_r = open(FIFONAME, O_RDONLY)) == -1)
    {
        perror("fail to open");
        exit(1);
    }
    if((fd_w = open(FIFONAME2, O_WRONLY)) == -1)
    {
        perror("fail to open");
        exit(1);
    }

    char buf[128] = "";
    size_t bytes;

    while (1)
    {
        if((bytes = read(fd_r, buf, sizeof(buf))) == -1)
        {
            perror("fail to read");
            exit(1);
        }

        printf("from send: %s\n", buf);

        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) -1] = '\0';

        write(fd_w, buf, sizeof(buf));
        /*
        if((bytes = write(fd_w, buf, sizeof(buf))) == -1)
        {
            perror("fail to write");
            exit(1);
        }
        */

    }

    return 0;
}



有名管道的读写规律-阻塞

有名管道的读写规律(阻塞)

  • 读写端都存在,只读不写
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>

int main()
{

    if(mkfifo("myfifo", 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    // 读写端都存在,只读不写
    // 如果原本管道中有数据,则正常读取
    // 如果管道中没有数据,则read函数会阻塞等待
    int fd;

    if((fd = open("myfifo", O_RDWR)) == -1)
    {
        perror("fail to open");
        exit(1);
    }

    write(fd, "hello world", 11);

    char buf[128] = "";
    read(fd, buf, sizeof(buf));
    printf("buf = %s\n", buf);

    read(fd, buf, sizeof(buf));
    printf("buf = %s\n", buf);

    return 0;
}



  • 读写端都存在,只写不读
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>

int main()
{

    if(mkfifo("myfifo", 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    // 读写端都存在,只写不读
    // 当有名管道的缓冲区写满后,write函数会发生阻塞
    // 默认有名管道的缓冲区为64K字节
    int fd;

    if((fd = open("myfifo", O_RDWR)) == -1)
    {
        perror("fail to open");
        exit(1);
    }

    int num = 0;
    while (1)
    {
        write(fd, "", 1024);
        num++;
        printf("num = %d\n", num);
    }

    return 0;
}



  • 在一个进程中,只有读端,没有写端
    如下案例:会在open函数的位置阻塞
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>

int main()
{

    if(mkfifo("myfifo", 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    // 只有读端,没有写端

    int fd;

    // 在一个进程中,只有读端,没有写端
    // 会在open函数的位置阻塞

    if((fd = open("myfifo", O_RDONLY)) == -1)
    {
        perror("fail to open");
        exit(1);
    }

    char buf[128] = "";
    size_t bytes;

	while(1)
	{
    if((bytes =read(fd, buf, sizeof(buf))) == -1)
    {
        perror("fail to read");
        exit(1);
    }

    printf("bytes = %ld\n", bytes);
    printf("buf = %s\n", buf);
	}
	
    return 0;
}



  • 在一个进程中,只有写端,没有读端
    如下案例:会在open函数的位置阻塞
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<errno.h>

int main()
{

    if(mkfifo("myfifo", 0664) == -1)
    {
        if(errno != EEXIST)
        {
            perror("fail to mkfifo");
            exit(1);
        }

    }

    // 只有写端,没有读端

    int fd;

    // 在一个进程中,只有写端,没有读端
    // 会在open函数的位置阻塞

    if((fd = open("myfifo", O_WRONLY)) == -1)
    {
        perror("fail to open");
        exit(1);
    }
    
	while(1)
	{
    write(fd, "hello world",11);
    printf("666\n");
	}
	
    return 0;
}



  • 一个进程只读,一个进程只写

    • 在一个进程中,只有读端,没有写端
    • 在一个进程中,只有写端,没有读端

注意:将上面两个代码一起运行,保证有名管道读写端都存在,就不会再open函数的地方阻塞了

注意
- 如果一个进程只读,一个进程只写,都运行后,如果关闭写端,读端read会返回0
- 如果一个进程只读,一个进程只写,都运行后,如果关闭读端,写端会立即产生SIGPIPE信号,默认的处理方式是退出进程

有名管道open设置非阻塞后的读写规律

有名管道open设置非阻塞后的读写规律

指定O_NONBLOCK(即open位或O_NONBLOCK)

  • 1、先以只读方式打开,如果没有进程已经为写而打开一个FIFO,只读open成功,并且open不阻塞
  • 2、先以只写方式打开,如果没有进程已经为读而打开一个FIFO,只写open将出错返回-1
  • 3、read、write读写命名管道中读数据时不阻塞
  • 4、通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE信号)退出
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值