一. 前言
上文中我們介紹了進程間通信的方法之一:信號,本文將繼續介紹另一種進程間通信的方法,即管道。管道是Linux中使用shell經常用到的一個技術,本文將深入剖析管道的實現和運行邏輯。
二. 管道簡介
在Linux的日常使用中,我們常常會用到管道,如下所示
ps -ef | grep 關鍵字 | awk '{print $2}' | xargs kill -9
這里面的豎線|就是一個管道。它會將前一個命令的輸出,作為后一個命令的輸入。從管道的這個名稱可以看出來,管道是一種單向傳輸數據的機制,它其實是一段緩存,里面的數據只能從一端寫入,從另一端讀出。如果想互相通信,我們需要創建兩個管道才行。
管道分為兩種類型,| 表示的管道稱為匿名管道,意思就是這個類型的管道沒有名字,用完了就銷毀了。就像上面那個命令里面的一樣,豎線代表的管道隨著命令的執行自動創建、自動銷毀。用戶甚至都不知道自己在用管道這種技術,就已經解決了問題。另外一種類型是命名管道。這個類型的管道需要通過 mkfifo 命令顯式地創建。
mkfifo hello
我們可以往管道里面寫入東西。例如,寫入一個字符串。
# echo "hello world" > hello
這個時候管道里面的內容沒有被讀出,這個命令就會停在這里。這個時候,我們就需要重新連接一個終端。在終端中用下面的命令讀取管道里面的內容:
# cat < hello hello world
一方面,我們能夠看到,管道里面的內容被讀取出來,打印到了終端上;另一方面,echo 那個命令正常退出了。這就是有名管道的執行流程。
【文章福利】小編推薦自己的Linux內核技術交流群:【865977150】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!!
三. 匿名管道創建
實際管道的創建調用的是系統調用pipe(),該函數建了一個管道 pipe,返回了兩個文件描述符,這表示管道的兩端,一個是管道的讀取端描述符 fd[0],另一個是管道的寫入端描述符 fd[1]。
int pipe(int fd[2])
其內核實現如下所示,pipe2 ()調用 __do_pipe_flags() 創建一個數組 files來存放管道的兩端的打開文件,另一個數組 fd 存放管道的兩端的文件描述符。如果 __do_pipe_flags() 沒有錯誤,那就調用fd_install()將兩個fd和兩個struct file關聯起來,這一點和打開一個文件的過程類似。
SYSCALL_DEFINE1(pipe, int __user *, fildes)
{
return sys_pipe2(fildes, 0);
}
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
struct file *files[2];
int fd[2];
int error;
error = __do_pipe_flags(fd, files, flags);
if (!error) {
if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) {
......
error = -EFAULT;
} else {
fd_install(fd[0], files[0]);
fd_install(fd[1], files[1]);
}
}
return error;
}
__do_pipe_flags()調用了create_pipe_files()生成fd,然后調用get_unused_fd_flags()賦值fdr和fdw,即讀文件描述符和寫文件描述符。由此也可以看出管道的特性:由一端寫入,由另一端讀出。
static int __do_pipe_flags(int *fd, struct file **files, int flags)
{
int error;
int fdw, fdr;
......
error = create_pipe_files(files, flags);
......
error = get_unused_fd_flags(flags);
......
fdr = error;
error = get_unused_fd_flags(flags);
......
fdw = error;
audit_fd_pair(fdr, fdw);
fd[0] = fdr;
fd[1] = fdw;
return 0;
......
}
create_pipe_files()是管道創建的關鍵邏輯,從這里可以看出來管道實際上也是一種抽象的文件系統pipefs,有著對應的特殊文件以及inode。這里首先通過get_pipe_inode()獲取特殊inode,然后調用alloc_file_pseudo()通過inode以及對應的掛載結構體pipe_mnt,文件操作結構體pipefifo_fops創建關聯的dentry并以此創建文件結構體并分配內存,通過alloc_file_clone()創建一份新的file后將兩個文件分別保存在res[0]和res[1]中。
int create_pipe_files(struct file **res, int flags)
{
struct inode *inode = get_pipe_inode();
struct file *f;
if (!inode)
return -ENFILE;
f = alloc_file_pseudo(inode, pipe_mnt, "",
O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)),
&pipefifo_fops);
if (IS_ERR(f)) {
free_pipe_info(inode->i_pipe);
iput(inode);
return PTR_ERR(f);
}
f->private_data = inode->i_pipe;
res[0] = alloc_file_clone(f, O_RDONLY | (flags & O_NONBLOCK),
&pipefifo_fops);
if (IS_ERR(res[0])) {
put_pipe_info(inode, inode->i_pipe);
fput(f);
return PTR_ERR(res[0]);
}
res[0]->private_data = inode->i_pipe;
res[1] = f;
return 0;
}
其虛擬文件系統pipefs對應的結構體和操作如下:
static struct file_system_type pipe_fs_type = {
.name = "pipefs",
.mount = pipefs_mount,
.kill_sb = kill_anon_super,
};
static int __init init_pipe_fs(void)
{
int err = register_filesystem(&pipe_fs_type);
if (!err) {
pipe_mnt = kern_mount(&pipe_fs_type);
}
......
}
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
};
static struct inode * get_pipe_inode(void)
{
struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
struct pipe_inode_info *pipe;
......
inode->i_ino = get_next_ino();
pipe = alloc_pipe_info();
......
inode->i_pipe = pipe;
pipe->files = 2;
pipe->readers = pipe->writers = 1;
inode->i_fop = &pipefifo_fops;
inode->i_state = I_DIRTY;
inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
return inode;
......
}
至此,一個匿名管道就創建成功了。如果對于 fd[1]寫入,調用的是 pipe_write(),向 pipe_buffer 里面寫入數據;如果對于 fd[0]的讀入,調用的是 pipe_read(),也就是從 pipe_buffer 里面讀取數據。至此,我們在一個進程內創建了管道,但是尚未實現進程間通信。
四. 匿名管道通信
在上文中我們提到了匿名管道通過|符號實現進程間的通信,傳遞輸入給下一個進程作為輸出,其實現原理如下:
- 利用fork創建子進程,復制file_struct會同樣復制fd輸入輸出數組,但是fd指向的文件僅有一份,即兩個進程間可以通過fd數組實現對同一個管道文件的跨進程讀寫操作
- 禁用父進程的讀,禁用子進程的寫,即從父進程寫入從子進程讀出,從而實現了單向管道,避免了混亂
- 對于A|B來說,shell首先創建子進程A,接著創建子進程B,由于二者均從shell創建,因此共用fd數組。shell關閉讀寫,A開寫B開讀,從而實現了A 和B之間的通信。
接著我們需要調用dup2()實現輸入輸出和管道兩端的關聯,該函數會將fd賦值給fd2
/* Duplicate FD to FD2, closing the old FD2 and making FD2 be
open the same file as FD is. Return FD2 or -1. */
int
__dup2 (int fd, int fd2)
{
if (fd < 0 || fd2 < 0)
{
__set_errno (EBADF);
return -1;
}
if (fd == fd2)
/* No way to check that they are valid. */
return fd2;
__set_errno (ENOSYS);
return -1;
}
在 files_struct 里面,有這樣一個表,下標是 fd,內容指向一個打開的文件 struct file。在這個表里面,前三項是定下來的,其中第零項 STDIN_FILENO 表示標準輸入,第一項 STDOUT_FILENO 表示標準輸出,第三項 STDERR_FILENO 表示錯誤輸出。
struct files_struct {
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
}
- 在 A 進程寫入端通過dup2(fd[1],STDOUT_FILENO)將 STDOUT_FILENO(也即第一項)不再指向標準輸出,而是指向創建的管道文件,那么以后往標準輸出寫入的任何東西,都會寫入管道文件。
- 在 B 進程中讀取端通過dup2(fd[0],STDIN_FILENO)將 STDIN_FILENO 也即第零項不再指向標準輸入,而是指向創建的管道文件,那么以后從標準輸入讀取的任何東西,都來自于管道文件。
至此,我們將 A|B 的功能完成。
五. 有名管道
對于有名管道,我們需要通過mkfifo創建,實際調用__xmknod()函數,最終調用mknod(),和字符設備創建一樣。
/* Create a named pipe (FIFO) named PATH with protections MODE. */
int
mkfifo (const char *path, mode_t mode)
{
dev_t dev = 0;
return __xmknod (_MKNOD_VER, path, mode | S_IFIFO, &dev);
}
/* Create a device file named PATH, with permission and special bits MODE
and device number DEV (which can be constructed from major and minor
device numbers with the `makedev' macro above). */
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);
}
mknod 在字符設備那一節已經解析過了,先是通過 user_path_create() 對于這個管道文件創建一個 dentry,然后因為是 S_IFIFO,所以調用 vfs_mknod()。由于這個管道文件是創建在一個普通文件系統上的,假設是在 ext4 文件上,于是 vfs_mknod 會調用 ext4_dir_inode_operations 的 mknod,也即會調用 ext4_mknod()。
在 ext4_mknod() 中,ext4_new_inode_start_handle() 會調用 __ext4_new_inode(),在 ext4 文件系統上真的創建一個文件,但是會調用 init_special_inode(),創建一個內存中特殊的 inode,這個函數我們在字符設備文件中也遇到過,只不過當時 inode 的 i_fop 指向的是 def_chr_fops,這次換成管道文件了,inode 的 i_fop 變成指向 pipefifo_fops,這一點和匿名管道是一樣的。這樣,管道文件就創建完畢了。
接下來,要打開這個管道文件,我們還是會調用文件系統的 open() 函數。還是沿著文件系統的調用方式,一路調用到 pipefifo_fops 的 open() 函數,也就是 fifo_open()。在 fifo_open() 里面會創建 pipe_inode_info,這一點和匿名管道也是一樣的。這個結構里面有個成員是 struct pipe_buffer *bufs。我們可以知道,所謂的命名管道,其實是也是內核里面的一串緩存。接下來,對于命名管道的寫入,我們還是會調用 pipefifo_fops 的 pipe_write() 函數,向 pipe_buffer 里面寫入數據。對于命名管道的讀入,我們還是會調用 pipefifo_fops 的 pipe_read(),也就是從 pipe_buffer 里面讀取數據。
static int fifo_open(struct inode *inode, struct file *filp)
{
struct pipe_inode_info *pipe;
bool is_pipe = inode->i_sb->s_magic == PIPEFS_MAGIC;
int ret;
filp->f_version = 0;
spin_lock(&inode->i_lock);
if (inode->i_pipe) {
pipe = inode->i_pipe;
pipe->files++;
spin_unlock(&inode->i_lock);
} else {
spin_unlock(&inode->i_lock);
pipe = alloc_pipe_info();
if (!pipe)
return -ENOMEM;
pipe->files = 1;
spin_lock(&inode->i_lock);
if (unlikely(inode->i_pipe)) {
inode->i_pipe->files++;
spin_unlock(&inode->i_lock);
free_pipe_info(pipe);
pipe = inode->i_pipe;
} else {
inode->i_pipe = pipe;
spin_unlock(&inode->i_lock);
}
}
filp->private_data = pipe;
/* OK, we have a pipe and it's pinned down */
__pipe_lock(pipe);
/* We can only do regular read/write on fifos */
filp->f_mode &= (FMODE_READ | FMODE_WRITE);
switch (filp->f_mode) {
case FMODE_READ:
/*
* O_RDONLY
* POSIX.1 says that O_NONBLOCK means return with the FIFO
* opened, even when there is no process writing the FIFO.
*/
pipe->r_counter++;
if (pipe->readers++ == 0)
wake_up_partner(pipe);
if (!is_pipe && !pipe->writers) {
if ((filp->f_flags & O_NONBLOCK)) {
/* suppress EPOLLHUP until we have
* seen a writer */
filp->f_version = pipe->w_counter;
} else {
if (wait_for_partner(pipe, &pipe->w_counter))
goto err_rd;
}
}
break;
case FMODE_WRITE:
/*
* 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 (!is_pipe && (filp->f_flags & O_NONBLOCK) && !pipe->readers)
goto err;
pipe->w_counter++;
if (!pipe->writers++)
wake_up_partner(pipe);
if (!is_pipe && !pipe->readers) {
if (wait_for_partner(pipe, &pipe->r_counter))
goto err_wr;
}
break;
case FMODE_READ | FMODE_WRITE:
/*
* 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.
*/
pipe->readers++;
pipe->writers++;
pipe->r_counter++;
pipe->w_counter++;
if (pipe->readers == 1 || pipe->writers == 1)
wake_up_partner(pipe);
break;
default:
ret = -EINVAL;
goto err;
}
/* Ok! */
__pipe_unlock(pipe);
return 0;
......
}
總結
無論是匿名管道還是命名管道,在內核都是一個文件。只要是文件就要有一個 inode。在這種特殊的 inode 里面,file_operations 指向管道特殊的 pipefifo_fops,這個 inode 對應內存里面的緩存。當我們用文件的 open 函數打開這個管道設備文件的時候,會調用 pipefifo_fops 里面的方法創建 struct file 結構,他的 inode 指向特殊的 inode,也對應內存里面的緩存,file_operations 也指向管道特殊的 pipefifo_fops。寫入一個 pipe 就是從 struct file 結構找到緩存寫入,讀取一個 pipe 就是從 struct file 結構找到緩存讀出。匿名管道和命名管道區別就在于匿名管道會通過dup2()指定輸入輸出源,完成之后立即釋放,而命名管道通過mkfifo創建掛載后,需要手動調用pipe_read()和pipe_write()來完成其功能,表現到用戶端即為前面提到的例子。
審核編輯:湯梓紅-
Linux
+關注
關注
87文章
11292瀏覽量
209329 -
管道
+關注
關注
3文章
145瀏覽量
17962 -
進程間通信
+關注
關注
0文章
16瀏覽量
2434
發布評論請先 登錄
相關推薦
評論