一. 前言
上文中我們介紹了進(jìn)程間通信的方法之一:信號,本文將繼續(xù)介紹另一種進(jìn)程間通信的方法,即管道。管道是Linux中使用shell經(jīng)常用到的一個技術(shù),本文將深入剖析管道的實(shí)現(xiàn)和運(yùn)行邏輯。
二. 管道簡介
在Linux的日常使用中,我們常常會用到管道,如下所示
ps -ef | grep 關(guān)鍵字 | awk '{print $2}' | xargs kill -9
這里面的豎線|就是一個管道。它會將前一個命令的輸出,作為后一個命令的輸入。從管道的這個名稱可以看出來,管道是一種單向傳輸數(shù)據(jù)的機(jī)制,它其實(shí)是一段緩存,里面的數(shù)據(jù)只能從一端寫入,從另一端讀出。如果想互相通信,我們需要創(chuàng)建兩個管道才行。
管道分為兩種類型,| 表示的管道稱為匿名管道,意思就是這個類型的管道沒有名字,用完了就銷毀了。就像上面那個命令里面的一樣,豎線代表的管道隨著命令的執(zhí)行自動創(chuàng)建、自動銷毀。用戶甚至都不知道自己在用管道這種技術(shù),就已經(jīng)解決了問題。另外一種類型是命名管道。這個類型的管道需要通過 mkfifo 命令顯式地創(chuàng)建。
mkfifo hello
我們可以往管道里面寫入東西。例如,寫入一個字符串。
# echo "hello world" > hello
這個時候管道里面的內(nèi)容沒有被讀出,這個命令就會停在這里。這個時候,我們就需要重新連接一個終端。在終端中用下面的命令讀取管道里面的內(nèi)容:
# cat < hello hello world
一方面,我們能夠看到,管道里面的內(nèi)容被讀取出來,打印到了終端上;另一方面,echo 那個命令正常退出了。這就是有名管道的執(zhí)行流程。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【865977150】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!!
三. 匿名管道創(chuàng)建
實(shí)際管道的創(chuàng)建調(diào)用的是系統(tǒng)調(diào)用pipe(),該函數(shù)建了一個管道 pipe,返回了兩個文件描述符,這表示管道的兩端,一個是管道的讀取端描述符 fd[0],另一個是管道的寫入端描述符 fd[1]。
int pipe(int fd[2])
其內(nèi)核實(shí)現(xiàn)如下所示,pipe2 ()調(diào)用 __do_pipe_flags() 創(chuàng)建一個數(shù)組 files來存放管道的兩端的打開文件,另一個數(shù)組 fd 存放管道的兩端的文件描述符。如果 __do_pipe_flags() 沒有錯誤,那就調(diào)用fd_install()將兩個fd和兩個struct file關(guān)聯(lián)起來,這一點(diǎn)和打開一個文件的過程類似。
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()調(diào)用了create_pipe_files()生成fd,然后調(diào)用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()是管道創(chuàng)建的關(guān)鍵邏輯,從這里可以看出來管道實(shí)際上也是一種抽象的文件系統(tǒng)pipefs,有著對應(yīng)的特殊文件以及inode。這里首先通過get_pipe_inode()獲取特殊inode,然后調(diào)用alloc_file_pseudo()通過inode以及對應(yīng)的掛載結(jié)構(gòu)體pipe_mnt,文件操作結(jié)構(gòu)體pipefifo_fops創(chuàng)建關(guān)聯(lián)的dentry并以此創(chuàng)建文件結(jié)構(gòu)體并分配內(nèi)存,通過alloc_file_clone()創(chuàng)建一份新的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;
}
其虛擬文件系統(tǒng)pipefs對應(yīng)的結(jié)構(gòu)體和操作如下:
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;
......
}
至此,一個匿名管道就創(chuàng)建成功了。如果對于 fd[1]寫入,調(diào)用的是 pipe_write(),向 pipe_buffer 里面寫入數(shù)據(jù);如果對于 fd[0]的讀入,調(diào)用的是 pipe_read(),也就是從 pipe_buffer 里面讀取數(shù)據(jù)。至此,我們在一個進(jìn)程內(nèi)創(chuàng)建了管道,但是尚未實(shí)現(xiàn)進(jìn)程間通信。
四. 匿名管道通信
在上文中我們提到了匿名管道通過|符號實(shí)現(xiàn)進(jìn)程間的通信,傳遞輸入給下一個進(jìn)程作為輸出,其實(shí)現(xiàn)原理如下:
- 利用fork創(chuàng)建子進(jìn)程,復(fù)制file_struct會同樣復(fù)制fd輸入輸出數(shù)組,但是fd指向的文件僅有一份,即兩個進(jìn)程間可以通過fd數(shù)組實(shí)現(xiàn)對同一個管道文件的跨進(jìn)程讀寫操作
- 禁用父進(jìn)程的讀,禁用子進(jìn)程的寫,即從父進(jìn)程寫入從子進(jìn)程讀出,從而實(shí)現(xiàn)了單向管道,避免了混亂
- 對于A|B來說,shell首先創(chuàng)建子進(jìn)程A,接著創(chuàng)建子進(jìn)程B,由于二者均從shell創(chuàng)建,因此共用fd數(shù)組。shell關(guān)閉讀寫,A開寫B(tài)開讀,從而實(shí)現(xiàn)了A 和B之間的通信。
接著我們需要調(diào)用dup2()實(shí)現(xiàn)輸入輸出和管道兩端的關(guān)聯(lián),該函數(shù)會將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 里面,有這樣一個表,下標(biāo)是 fd,內(nèi)容指向一個打開的文件 struct file。在這個表里面,前三項是定下來的,其中第零項 STDIN_FILENO 表示標(biāo)準(zhǔn)輸入,第一項 STDOUT_FILENO 表示標(biāo)準(zhǔn)輸出,第三項 STDERR_FILENO 表示錯誤輸出。
struct files_struct {
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
}
- 在 A 進(jìn)程寫入端通過dup2(fd[1],STDOUT_FILENO)將 STDOUT_FILENO(也即第一項)不再指向標(biāo)準(zhǔn)輸出,而是指向創(chuàng)建的管道文件,那么以后往標(biāo)準(zhǔn)輸出寫入的任何東西,都會寫入管道文件。
- 在 B 進(jìn)程中讀取端通過dup2(fd[0],STDIN_FILENO)將 STDIN_FILENO 也即第零項不再指向標(biāo)準(zhǔn)輸入,而是指向創(chuàng)建的管道文件,那么以后從標(biāo)準(zhǔn)輸入讀取的任何東西,都來自于管道文件。
至此,我們將 A|B 的功能完成。
五. 有名管道
對于有名管道,我們需要通過mkfifo創(chuàng)建,實(shí)際調(diào)用__xmknod()函數(shù),最終調(diào)用mknod(),和字符設(shè)備創(chuàng)建一樣。
/* 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 在字符設(shè)備那一節(jié)已經(jīng)解析過了,先是通過 user_path_create() 對于這個管道文件創(chuàng)建一個 dentry,然后因?yàn)槭?S_IFIFO,所以調(diào)用 vfs_mknod()。由于這個管道文件是創(chuàng)建在一個普通文件系統(tǒng)上的,假設(shè)是在 ext4 文件上,于是 vfs_mknod 會調(diào)用 ext4_dir_inode_operations 的 mknod,也即會調(diào)用 ext4_mknod()。
在 ext4_mknod() 中,ext4_new_inode_start_handle() 會調(diào)用 __ext4_new_inode(),在 ext4 文件系統(tǒng)上真的創(chuàng)建一個文件,但是會調(diào)用 init_special_inode(),創(chuàng)建一個內(nèi)存中特殊的 inode,這個函數(shù)我們在字符設(shè)備文件中也遇到過,只不過當(dāng)時 inode 的 i_fop 指向的是 def_chr_fops,這次換成管道文件了,inode 的 i_fop 變成指向 pipefifo_fops,這一點(diǎn)和匿名管道是一樣的。這樣,管道文件就創(chuàng)建完畢了。
接下來,要打開這個管道文件,我們還是會調(diào)用文件系統(tǒng)的 open() 函數(shù)。還是沿著文件系統(tǒng)的調(diào)用方式,一路調(diào)用到 pipefifo_fops 的 open() 函數(shù),也就是 fifo_open()。在 fifo_open() 里面會創(chuàng)建 pipe_inode_info,這一點(diǎn)和匿名管道也是一樣的。這個結(jié)構(gòu)里面有個成員是 struct pipe_buffer *bufs。我們可以知道,所謂的命名管道,其實(shí)是也是內(nèi)核里面的一串緩存。接下來,對于命名管道的寫入,我們還是會調(diào)用 pipefifo_fops 的 pipe_write() 函數(shù),向 pipe_buffer 里面寫入數(shù)據(jù)。對于命名管道的讀入,我們還是會調(diào)用 pipefifo_fops 的 pipe_read(),也就是從 pipe_buffer 里面讀取數(shù)據(jù)。
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;
......
}
總結(jié)
無論是匿名管道還是命名管道,在內(nèi)核都是一個文件。只要是文件就要有一個 inode。在這種特殊的 inode 里面,file_operations 指向管道特殊的 pipefifo_fops,這個 inode 對應(yīng)內(nèi)存里面的緩存。當(dāng)我們用文件的 open 函數(shù)打開這個管道設(shè)備文件的時候,會調(diào)用 pipefifo_fops 里面的方法創(chuàng)建 struct file 結(jié)構(gòu),他的 inode 指向特殊的 inode,也對應(yīng)內(nèi)存里面的緩存,file_operations 也指向管道特殊的 pipefifo_fops。寫入一個 pipe 就是從 struct file 結(jié)構(gòu)找到緩存寫入,讀取一個 pipe 就是從 struct file 結(jié)構(gòu)找到緩存讀出。匿名管道和命名管道區(qū)別就在于匿名管道會通過dup2()指定輸入輸出源,完成之后立即釋放,而命名管道通過mkfifo創(chuàng)建掛載后,需要手動調(diào)用pipe_read()和pipe_write()來完成其功能,表現(xiàn)到用戶端即為前面提到的例子。
審核編輯:湯梓紅-
Linux
+關(guān)注
關(guān)注
87文章
11161瀏覽量
208460 -
管道
+關(guān)注
關(guān)注
3文章
145瀏覽量
17906 -
進(jìn)程間通信
+關(guān)注
關(guān)注
0文章
16瀏覽量
2424
發(fā)布評論請先 登錄
相關(guān)推薦
評論