管道 - 多进程编程(一)

前一章提到了一个shell命令:ps -ef | grep demo,
这里的 | 其实就是一个管道,shell创建了两个进程来分别执行 ps -ef 和 grep demo,并将前一个的输出,作为输入给到第二个。

在这里插入图片描述
特点:
1、管道是一个在内核内存中维护的缓冲区,这个缓冲区的存储能力是有限的,不同操作系统的大小不一定相同(Linux64位系统下其大小是4k),可以使用shell命令:ulimit -a查看
2、管道拥有文件的特质:读操作、写操作,但是没有文件实体。
3、管道只能承载无格式字节流以及缓冲区大小受限
4、通过管道传递的数据是顺序的,读取与写入的顺序保持一致
5、传递是单向的,如果想要双向通信,就要创建两个管道,也就是要么父进程写入,子进程读取;要么父进程读取,子进程写入
6、只能在具有公共祖先的进程之间使用(父子进程,兄弟进程,具有亲缘关系的进程)

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

1、特点:

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

一、管道

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

1、特点:

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

2、原型:

1 #include <unistd.h>
2 int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

要关闭管道只需将这两个文件描述符关闭即可。

3、例子

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:

若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

1. 匿名管道

1.1 匿名管道(pipe)

无名管道pipe使用头文件<unistd.h>文件,使用过程中使用一个两个大小的数组进行读写管道的观念里

#include <unistd.h>
int pipe(int pipefd[2]);
- pipefd[0] : 管道读取端
- pipefd[1] : 管道写入端
- 返回值:
    创建成功返回 0
    创建失败返回 -1

实例:

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

int main() {

    // 创建管道,双向管道第一个是管道,第二个是写管道
    int pipe_fd[2] = {0};
    int res_p = pipe(pipe_fd);
    if (res_p == 0) {
        printf("pipe create success\n");
    } else {
        perror("pipe");
    }

    // 创建子进程
    pid_t res = fork();

    // 父进程写入数据
    if (res > 0) 
    {    
        printf("Parent: %d. ", getpid());
        close(pipe_fd[0]); // 关闭读取端
        char buf_w[] = "Hello, world";
        write(pipe_fd[1], buf_w, strlen(buf_w));
		printf("Writebuf: %s\n", buf_w);
    } 
    else if (res == 0)  // 子进程读取数据
    {
        printf("Child: %d. ", getpid());
        close(pipe_fd[1]); // 关闭写入端
        char buf_r[100];
        read(pipe_fd[0], buf_r, 100);
        printf("Readbuf: %s\n", buf_r);
    } 
    else 
    {
        perror("fork");
    }

    return 0;
}
运行测试结果
pipe create success
Parent: 14729. Writebuf: Hello, world
Child: 14730. Readbuf: Hello, world

1.2 延伸阅读

2、有名管道(FIFO)

匿名管道只能用于具有亲缘关系的进程间通信。为了客服这个缺点,提出了有名管道(FIFO)。
有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够通过FIFO相互通信。FIFO,也称为命名管道,它是一种文件类型。

1、特点

  1. FIFO可以在无关的进程之间交换数据,与无名管道不同。

  2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2、原型

1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。

  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

3、例子

FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用 FIFO 进行 IPC 的过程:

2.1 有名管道的实现方式

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- pathname: FIFO文件的路径 或者是 想要保存的路径
- mode: 权限
- 返回值:
    成功:0
    失败:-1

有名管道可以用于不同进程之间消息通信,这里给出了两个不同的进程文件,一个进程文件用来向FIFO中写数据,另外一个进程从FIFO中读取数据:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
 
#define FIFO_PATH	"/tmp/myfifo"
 
int main(){
	int pipe_fd = -1;
	unsigned char buff[]="hello world";

	// 确定当前管道是否存在,如果不存在创建一个管道
	if (access(FIFO_PATH, F_OK) == -1){
		printf("FIFO file not exist, creat it\n");
		if(mkfifo(FIFO_PATH, 0777) < 0){
			perror("mkfifo");
			_exit(-1);
		}
	}
	
    // 打开管道,获得管道文件描述符
	pipe_fd = open(FIFO_PATH, O_WRONLY);
	if(pipe_fd < 0){
		perror("open");
		printf("open file failed! %d\n", pipe_fd);
		return -1;
	}
	
	// 循环向FIFO管道中定时写数据
	while(1){
		if(write(pipe_fd, buff, sizeof(buff)) < 0){
			printf("write error!\n");
			close(pipe_fd);
			return -1;
		}else{
			printf("send to fifo success!\n");
		}
		sleep(1);
	}
	close(pipe_fd);
 
	return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
 
#define FIFO_PATH	"/tmp/myfifo"
 
int main(void){
	int fd;
	unsigned char buff[50];
    
	// 确定当前管道是否存在,如果不存在创建一个管道
	if (access(FIFO_PATH, F_OK) == -1){
		printf("FIFO_PATH file not exist, creat it\n");
		if(mkfifo(FIFO_PATH, 0777) < 0){
			perror("mkfifo");
			_exit(-1);
		}
	}
	
	if((fd = open(FIFO_PATH, O_RDONLY|O_NONBLOCK, 0)) < 0){
		perror("open");
		return -1;
	}
	
	// 循环定时从FIFO中读取数据
	while(1){
		memset(buff, 0x00, sizeof(buff));
		if(read(fd, buff, sizeof(buff)) < 0){
			printf("read from fifo failed!\n");
			continue;
		}
		printf("receive:%s\n",  buff);
		sleep(1);
	}
	close(fd);
	return 0;
}

一旦创建了FIFO,就可以使用 open 打开它,常见的 I/O 函数都可用于 FIFO

2.2、延申阅读

 知道了上面的有名管道实例,那这么做的原因是什么呢?我们都知道Linux不同进程运行的物理空间都是分开的,那么两个进程之间如果想直接通信是不可能做到的,这个时候就需要一个“梯子”来作为双方沟通的桥梁。 要分析梯子,我们先看函数mkfifo的实现,来探探Linux实现这个特殊的文件是怎么做到的。

        查看mkfifo需要看glibc的源码,先看定义:

       glibc/sysdeps/posix/mkfifo.c

int
mkfifo (const char *path, mode_t mode)
{
  dev_t dev = 0;
  return __xmknod (_MKNOD_VER, path, mode | S_IFIFO, &dev);
}
        这里可以看到它最终调用了xmknod方法,去创建节点,来看看mknod的实现

int
__xmknod (int vers, const char *path, mode_t mode, dev_t *dev)
{
  unsigned long long int k_dev;
  if (vers != _MKNOD_VER)
    return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
  /* We must convert the value to dev_t type used by the kernel.  */
  k_dev =  (*dev) & ((1ULL << 32) - 1);
  if (k_dev != *dev)
    return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
  return INLINE_SYSCALL (mknod, 3, path, mode, (unsigned int) k_dev);
}
        最终通过INLINE_SYSCALL (mknod, 3, path, mode, (unsigned int) k_dev);实现mknod的调用。继续看mknod实现linux/security/inode.c

static int mknod(struct inode *dir, struct dentry *dentry,
             int mode, dev_t dev)
{
    struct inode *inode;
    int error = -EPERM;
 
    if (dentry->d_inode)
        return -EEXIST;
 
    inode = get_inode(dir->i_sb, mode, dev);
    if (inode) {
        d_instantiate(dentry, inode);
        dget(dentry);
        error = 0;
    }
    return error;
}

        首先判断inode是否存在,说到这里,Linux下的文件都依赖于一个很重要的结构体inode,他们都是通过inode产生关联,感兴趣的可以继续深挖inode结构体。接下来继续看看get_inode这个方法实现。

static struct inode *get_inode(struct super_block *sb, int mode, dev_t dev)
{
    struct inode *inode = new_inode(sb);
 
    if (inode) {
        inode->i_mode = mode;
        inode->i_uid = 0;
        inode->i_gid = 0;
        inode->i_blocks = 0;
        inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
        switch (mode & S_IFMT) {
        default:
            init_special_inode(inode, mode, dev);
            break;
        case S_IFREG:
            inode->i_fop = &default_file_ops;
            break;
        case S_IFDIR:
            inode->i_op = &simple_dir_inode_operations;
            inode->i_fop = &simple_dir_operations;
 
            /* directory inodes start off with i_nlink == 2 (for "." entry) */
            inc_nlink(inode);
            break;
        }
    }
    return inode;
}

        到这里我们可以看到inode的一些初始化动作,包括赋予它创建时间,我们通过命令行或者相关api查看文件完整信息的时候就包括创建时间,就是在这里写入的。继续看针对mode的处理,S_IFREG是普通文件,S_IFDIR是普通的目录,而我们fifo创建的当然是不一样的文件,这里可以看到普通文件和目录,都会对inode->i_fop赋予文件操作函数,为了避免展开太多,我们只看fifo这个特殊文件节点的。

        

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
    inode->i_mode = mode;
    if (S_ISCHR(mode)) {
        inode->i_fop = &def_chr_fops;
        inode->i_rdev = rdev;
    } else if (S_ISBLK(mode)) {
        inode->i_fop = &def_blk_fops;
        inode->i_rdev = rdev;
    } else if (S_ISFIFO(mode))
        inode->i_fop = &def_fifo_fops;
    else if (S_ISSOCK(mode))
        inode->i_fop = &bad_sock_fops;
    else
        printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",
               mode);
}
EXPORT_SYMBOL(init_special_inode);

        千呼万唤始出来,终于看到了我们的fifo文件操作指针def_fifo_fops,顺便我们也可以看到其它三个特殊的,字符文件、块设备文件、sock文件,关注我们的重点对象def_fifo_fops。

const struct file_operations def_fifo_fops = {
    .open        = fifo_open,    /* will set read_ or write_pipefifo_fops */
};
        继续看fifo_open实现。

static int fifo_open(struct inode *inode, struct file *filp)
{
    struct pipe_inode_info *pipe;
    int ret;
 
    mutex_lock(&inode->i_mutex);
    pipe = inode->i_pipe;
    if (!pipe) {
        ret = -ENOMEM;
        pipe = alloc_pipe_info(inode);
        if (!pipe)
            goto err_nocleanup;
        inode->i_pipe = pipe;
    }
    filp->f_version = 0;
 
    /* We can only do regular read/write on fifos */
    filp->f_mode &= (FMODE_READ | FMODE_WRITE);
 
    switch (filp->f_mode) {
    case 1:
    /*
     *  O_RDONLY
     *  POSIX.1 says that O_NONBLOCK means return with the FIFO
     *  opened, even when there is no process writing the FIFO.
     */
        filp->f_op = &read_pipefifo_fops;
        pipe->r_counter++;
        if (pipe->readers++ == 0)
            wake_up_partner(inode);
 
        if (!pipe->writers) {
            if ((filp->f_flags & O_NONBLOCK)) {
                /* suppress POLLHUP until we have
                 * seen a writer */
                filp->f_version = pipe->w_counter;
            } else 
            {
                wait_for_partner(inode, &pipe->w_counter);
                if(signal_pending(current))
                    goto err_rd;
            }
        }
        break;
    
    case 2:
    /*
     *  O_WRONLY
     *  POSIX.1 says that O_NONBLOCK means return -1 with
     *  errno=ENXIO when there is no process reading the FIFO.
     */
        ret = -ENXIO;
        if ((filp->f_flags & O_NONBLOCK) && !pipe->readers)
            goto err;
 
        filp->f_op = &write_pipefifo_fops;
        pipe->w_counter++;
        if (!pipe->writers++)
            wake_up_partner(inode);
 
        if (!pipe->readers) {
            wait_for_partner(inode, &pipe->r_counter);
            if (signal_pending(current))
                goto err_wr;
        }
        break;
    
    case 3:
    /*
     *  O_RDWR
     *  POSIX.1 leaves this case "undefined" when O_NONBLOCK is set.
     *  This implementation will NEVER block on a O_RDWR open, since
     *  the process can at least talk to itself.
     */
        filp->f_op = &rdwr_pipefifo_fops;
 
        pipe->readers++;
        pipe->writers++;
        pipe->r_counter++;
        pipe->w_counter++;
        if (pipe->readers == 1 || pipe->writers == 1)
            wake_up_partner(inode);
        break;
 
    default:
        ret = -EINVAL;
        goto err;
    }
 
    /* Ok! */
    mutex_unlock(&inode->i_mutex);
    return 0;
 
err_rd:
    if (!--pipe->readers)
        wake_up_interruptible(&pipe->wait);
    ret = -ERESTARTSYS;
    goto err;
 
err_wr:
    if (!--pipe->writers)
        wake_up_interruptible(&pipe->wait);
    ret = -ERESTARTSYS;
    goto err;
 
err:
    if (!pipe->readers && !pipe->writers)
        free_pipe_info(inode);
 
err_nocleanup:
    mutex_unlock(&inode->i_mutex);
    return ret;
}

        这个open对应的就是我们上面fifo_read.c和fifo_write.c里面的open函数,回看调用的地方

pipe_fd = open(FIFO_PATH, O_WRONLY); //写管道的打开调用
fd = open(FIFO_PATH, O_RDONLY|O_NONBLOCK, 0) //读管道的打开调用

        从这里可以看出,open函数指定了读写权限,对应上面fifo_open函数实现,首先是获取inode里面的pipe成员(通过看inode源码,可以发现字符设备、块设备和管道设备属于一个union结构体)通过对open指定的mode解析可以看到,O_RDONLY、O_WRONLY、O_RDWR的处理主要是针对这几个成员的的处理

filp->f_op
pipe->r_counter
pipe->readers
pipe->w_counter
pipe->writers

        可以看到只读O_RDONLY的时候,r_counter自增1,readers自增1,如果readers自增之后还为0,wake_up_partner(inode),如果写writers为0,且没有指定O_NONBLOCK,就会等待w_counte的改变;同样,如果是只写O_WRONLY的时候,如果指定了O_NONBLOCK,但是没有读端存在,就立马返回错误,否则,w_counter自增,writers自增,继续判定读端是否存在,如果读端不存在就阻塞等待r_counter的改变。

        看看三个文件操作指针read_pipefifo_fops、write_pipefifo_fops、rdwr_pipefifo_fops

const struct file_operations read_pipefifo_fops = {
    .llseek        = no_llseek,
    .read        = do_sync_read,
    .aio_read    = pipe_read,
    .write        = bad_pipe_w,
    .poll        = pipe_poll,
    .unlocked_ioctl    = pipe_ioctl,
    .open        = pipe_read_open,
    .release    = pipe_read_release,
    .fasync        = pipe_read_fasync,
};
 
const struct file_operations write_pipefifo_fops = {
    .llseek        = no_llseek,
    .read        = bad_pipe_r,
    .write        = do_sync_write,
    .aio_write    = pipe_write,
    .poll        = pipe_poll,
    .unlocked_ioctl    = pipe_ioctl,
    .open        = pipe_write_open,
    .release    = pipe_write_release,
    .fasync        = pipe_write_fasync,
};
 
const struct file_operations rdwr_pipefifo_fops = {
    .llseek        = no_llseek,
    .read        = do_sync_read,
    .aio_read    = pipe_read,
    .write        = do_sync_write,
    .aio_write    = pipe_write,
    .poll        = pipe_poll,
    .unlocked_ioctl    = pipe_ioctl,
    .open        = pipe_rdwr_open,
    .release    = pipe_rdwr_release,
    .fasync        = pipe_rdwr_fasync,
};

        read的时候,主要实现do_sync_read,write主要实现do_sync_write,rw的时候,主要实现do_sync_read和do_sync_write,最终都会call pipe_read和pipe_write。

        pipe_read实现的是从kernel层到用户层数据的拷贝pipe_iov_copy_to_user,pipe_write实现的是从用户层到kernel层数据的拷贝pipe_iov_copy_from_user,至此已经看到了一条从user到kernel再回到user数据传输通道,也就是说这条路是通过Kernel这个“梯子”来实现的。
 

3. popen高级管道

Linux 中的popen机制可以在程序中执行一个shell命令,有两种操作模式,分别为读和写。在读模式中,程序中可以读取到命令的输出,其中有一个应用就是获取网络接口的参数。在写模式中,最常用的是创建一个新的文件或开启其他服务等。

3.1 popen实例

Linux 中的popen机制可以在程序中执行一个shell命令,有两种操作模式,分别为读和写。在读模式中,程序中可以读取到命令的输出,其中有一个应用就是获取网络接口的参数。在写模式中,最常用的是创建一个新的文件或开启其他服务等。

头文件

#include<stdio.h>

定义函数

FILE * popen( const char * command,const char * type);

函数说明

popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。此外,所有使用文件指针(FILE*)操作的函数也都可以使用,除了fclose()以外。

  • 如果 type 为 r,那么调用进程读进 command 的标准输出。
  • 如果 type 为 w,那么调用进程写到 command 的标准输入。

返回值

若成功则返回文件指针,否则返回NULL,错误原因存于errno中。

错误代码

EINVAL参数type不合法。

注意事项

在编写具SUID/SGID权限的程序时请尽量避免使用popen(),popen()会继承环境变量,通过环境变量可能会造成系统安全的问题。
#include <stdlib.h>
#include <stdio.h>
 
#define BUF_SIZE 1024
char buf[BUF_SIZE];
 
int main(void)
{
    FILE * p_file = NULL;
 
    p_file = popen("ifconfig enp5s0", "r");
    if (!p_file) {
        fprintf(stderr, "Erro to popen");
    }
 
    while (fgets(buf, BUF_SIZE, p_file) != NULL) {
        fprintf(stdout, "%s", buf);
    }
    pclose(p_file);
 
    p_file = popen("touch test.tmp", "w");
    if (!p_file) {
        fprintf(stderr, "Erro to popen");
    }
 
    while (fgets(buf, BUF_SIZE, p_file) != NULL) {
        fprintf(stdout, "%s", buf);
    }
    pclose(p_file);
 
    return 0;
}

3.2 扩展阅读

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值