内核角落 - 使用 IO 通道的简易 I/O
Glib 的 IO 通道提供了几个强大的功能
缓冲 I/O:与 C 标准库一样,IO 通道提供用户端缓冲,从而最大限度地减少系统调用次数,并确保以最佳大小的单元执行 I/O。
可移植性:IO 通道是可移植的,可在各种 UNIX 系统以及 Windows 中工作。
简单而高效的 I/O 例程:辅助例程使常见的编程任务(例如“准确读取一行”或“读取整个文件”)变得容易。
主循环集成:集成到 Glib 主循环中意味着多路复用 I/O 和事件驱动编程变得容易。
尽管 Glib 是为强大而复杂的 GNOME 应用程序设计的,但实际上它是一个独立于 GNOME 的库,可以很容易地从任何 C 应用程序中使用。
主循环,有时称为事件循环,允许单线程进程等待和处理来自多个源的事件。大多数 GUI 程序员都熟悉主循环:它们允许事件驱动的 GUI 编程注册回调函数,这些函数响应事件(例如按钮按下或窗口关闭)而被调用。Gtk+ 主循环构建在 Glib 主循环之上。
Glib 主循环使用多路复用 I/O 实现——在 Linux 中,通过 poll() 系统调用。事件与文件描述符关联,文件描述符通过 poll() 监视。通过这种方式,应用程序无需不断检查新事件,而是可以休眠,在没有活动时不会消耗处理器时间。
Glib 的主循环将回调函数与每个事件关联。当事件发生时,主循环将回调到给定的函数中。回调按照其关联事件发生的顺序调用,尽管可以赋予事件优先级来更改此顺序。由于可以监视多个事件并注册多个回调函数,因此即使是单线程进程也可以处理大量事件。
Glib 库是 GNOME 的基础库,提供可移植性包装器和一组辅助函数,以减少 C 编程的难度。尽管 Glib 是 GNOME 的一部分,但它本身非常有用,许多非 GNOME 项目确实在不接触 GNOME 任何其他部分的情况下使用了 Glib。事实上,Glib 甚至对控制台应用程序也有益。本文不假设使用其他 GNOME 组件;所涵盖的接口在复杂的 GNOME 应用程序和简单的控制台程序中都同样有效。
通过 pkg-config 程序可以轻松编译具有所需 Glib 支持的应用程序。您可以使用以下命令从源文件 gio.c 构建二进制文件 gio
gcc -Wall -O2 \ `pkg-config --cflags --libs glib-2.0` \ -o gio \ gio.c
IO 通道由 GIOChannel 数据结构表示。其字段是私有的,并且只能使用官方 IO 通道接口访问。
每个 IO 通道都与单个文件或其他“类文件”对象关联。在 Linux 上,IO 通道可以与任何打开的文件描述符关联,包括套接字和管道。一旦关联,IO 通道就用于访问文件。
监视是针对给定的 IO 通道创建的,以及一组要等待的事件和一个在响应时调用的回调函数。然后,监视与 Glib 的主循环集成。当事件发生时——例如,套接字有新的数据可供读取——监视被触发,并且回调会自动调用。
监视是 IO 通道强大功能的核心:应用程序可以创建多个监视并将它们与许多其他事件一起集成到 Glib 主循环中,从而为即使是简单的单线程应用程序也提供事件驱动的编程。
列表 1 是一个完整且可运行的控制台应用程序,它使用 IO 通道在两个管道之间进行通信。它创建了两个 IO 通道,一个用于管道的读取端,另一个用于管道的写入端。然后,它为这两个 IO 通道注册监视。一个监视在管道可用于读取时(即,当从管道的读取端读取不会阻塞时)调用回调 gio_in()。另一个监视在管道可用于写入时(即,当写入管道的写入端不会阻塞时)调用回调 gio_out()。gio_out() 回调将一条小消息写入管道。gio_in() 回调从管道读取可用数据并将其打印到标准输出。
列表 1. 一个完整且可运行的控制台应用程序,它使用 IO 通道在两个管道之间进行通信
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <glib.h> static gboolean gio_in (GIOChannel *gio, GIOCondition condition, gpointer data) { GIOStatus ret; GError *err = NULL; gchar *msg; gsize len; if (condition & G_IO_HUP) g_error ("Read end of pipe died!\n"); ret = g_io_channel_read_line (gio, &msg, &len, NULL, &err); if (ret == G_IO_STATUS_ERROR) g_error ("Error reading: %s\n", err->message); printf ("Read %u bytes: %s\n", len, msg); g_free (msg); return TRUE; } static gboolean gio_out (GIOChannel *gio, GIOCondition condition, gpointer data) { const gchar *msg = "The price of greatness is responsibility.\n"; GIOStatus ret; GError *err = NULL; gsize len; if (condition & G_IO_HUP) g_error ("Write end of pipe died!\n"); ret = g_io_channel_write_chars (gio, msg, -1, &len, &err); if (ret == G_IO_STATUS_ERROR) g_error ("Error writing: %s\n", err->message); printf ("Wrote %u bytes.\n", len); return TRUE; } void init_channels (void) { GIOChannel *gio_read, *gio_write; int fd[2], ret; ret = pipe (fd); if (ret == -1) g_error ("Creating pipe failed: %s\n", strerror (errno)); gio_read = g_io_channel_unix_new (fd[0]); gio_write = g_io_channel_unix_new (fd[1]); if (!gio_read || !gio_write) g_error ("Cannot create new GIOChannel!\n"); if (!g_io_add_watch (gio_read, G_IO_IN | G_IO_HUP, gio_in, NULL)) g_error ("Cannot add watch on GIOChannel!\n"); if (!g_io_add_watch (gio_write, G_IO_OUT | G_IO_HUP, gio_out, NULL)) g_error ("Cannot add watch on GIOChannel!\n"); } int main (void) { GMainLoop *loop = g_main_loop_new (NULL, FALSE); init_channels (); g_main_loop_run (loop); /* Wheee! */ return 0; }
可以肯定的是,这是一个完全基于解释的示例。在单个应用程序中像这样操作管道是愚蠢的。此外,程序将不断地从管道读取和写入(您可以使用 Ctrl-C 终止该进程)。尽管如此,此示例还是达到了一个很好的目的:它演示了事件驱动的编程和主循环多路复用 I/O 的实用性。此程序的自然扩展是将其分成两个进程,一个消费者和一个生产者,并在管道上实际进行进程间通信。向主循环添加少量其他 IO 通道、一些 GUI 事件、一些计时器等等,您将拥有一个真正的程序!
有两种方法可以创建新的 IO 通道。最简单的方法是从现有的打开的文件描述符创建 IO 通道。文件描述符可以映射到任何对象,包括套接字和管道
GIOChannel *gio; gio = g_io_channel_unix_new (fd); if (!gio) g_error ("Error creating new GIOChannel!\n");
顾名思义,此函数是 UNIX 特有的。另一种方法可用于以平台无关的方式创建 IO 通道
GIOChannel *gio; GError *err = NULL; gio = g_io_channel_new_file ("/etc/passwd" "r", &err); if (!gio) g_error ("Error creating new GIOChannel: %s\n", err->msg);
第二个参数指定打开文件的模式:r、w、r+、w+、a 或 a+ 之一。这些值的含义与 fopen() 相同。例如,在此代码片段中,我们要求创建一个只读 IO 通道。
在列表 1 的示例程序中,我们使用 g_io_channel_unix_new() 创建了两个 IO 通道,管道的每一端各一个。
给定一个 IO 通道,创建监视很容易
guint ret; ret = g_io_add_watch (gio, G_IO_IN, callback, NULL); if (!ret) g_error ("Error creating watch!\n");
第一个参数 gio 是我们要监视的 IO 通道。第二个参数是要监视的一个或多个条件的掩码。当有数据可供读取且不会阻塞时,条件 G_IO_IN 为真。其他条件包括 G_IO_OUT(数据可以写入且不会阻塞)、G_IO_PRI(紧急数据可供读取)、G_IO_ERR(发生错误)和 G_IO_HUP(连接已挂断)。第三个参数是 Glib 主循环在事件发生时将调用的回调函数。
监视回调采用以下形式
gboolean callback (GIOChannel *gio, GIOCondition condition, gpointer data);
其中 gio 是适用的 IO 通道,condition 是触发事件的位掩码,data 是传递给 g_io_add_watch() 的最后一个参数。
如果回调的返回值是 FALSE,则监视将自动删除。
在列表 1 的示例程序中,我们为每个 IO 通道创建了两个监视。
Glib 库提供了三个基本接口用于从 IO 通道读取。
第一个 g_io_channel_read_chars() 用于从 IO 通道读取指定数量的字符到预先分配的缓冲区中
GIOStatus g_io_read_chars (GIOChannel *channel, gchar *buf, gsize count, gsize *bytes_read, GError **error);
此函数从 IO 通道 channel 读取最多 count 字节到缓冲区 buf 中。成功返回后,bytes_read 将指向实际读取的字节数。如果失败,error 将指向 GError 结构。
返回值是一个整数,具有四个值之一:G_IO_STATUS_ERROR(发生错误)、G_IO_STATUS_NORMAL(成功)、G_IO_STATUS_EOF(已到达文件末尾)或 G_IO_STATUS_AGAIN(资源暂时不可用,请重试)。
第二个接口 g_io_channel_read_line() 用于从给定的 IO 通道读取整行。它将一直等待,直到读取以换行符分隔的行
GIOStatus g_io_channel_read_line (GIOChannel *channel, gchar **str_return, gsize *length, gsize *terminator_pos, GError **error);
成功返回后,str_return 将包含一个指向新分配的 length 字节内存块的指针。terminator_pos 是 str_return 中终止字符的偏移量。此函数返回的数据必须通过调用 g_free() 释放。
最后一个接口 g_io_channel_read_to_end() 将文件中的所有剩余数据读取到给定的缓冲区中
GIOStatus g_io_channel_read_to_end (GIOChannel *channel, gchar **str_return, gsize *length, GError **error);
成功返回后,str_return 将包含一个指向新分配的 length 字节内存块的指针,该内存块必须通过 g_free() 释放。
此函数不应在映射到文件描述符的 IO 通道上使用,这些文件描述符在数据耗尽时不一定会返回文件末尾。例如,我们不能在示例程序中使用此函数,因为管道在另一端关闭其连接端之前不会返回文件末尾。因此,如果我们使用 g_io_channel_read_to_end(),我们的示例将无限期地阻塞。
相反,在我们的示例中,我们使用 g_io_channel_read_line() 从管道读取整行。
Glib 提供了一个用于写入 IO 通道的接口
GIOStatus g_io_channel_write_chars (GIOChannel *channel, const gchar *buf, gssize count, gsize *bytes_written, GError **error);
成功调用 g_io_channel_write_chars() 将从 buf 指向的缓冲区写入最多 count 字节到 IO 通道 channel 表示的文件中。如果 count 为负一,则 buf 将被视为以 NULL 结尾的字符串。返回时,bytes_written 包含实际写入的字节数。
与 C 标准 I/O 库一样,IO 通道执行缓冲 I/O 以优化性能。因此,每次调用 g_io_channel_write_chars() 后,写入请求可能不会实际提交到内核。相反,glib 可能会等到足够大的缓冲区已满,然后在一个大的 swoopo 中提交写入请求。
g_io_channel_flush() 函数用于强制提交任何挂起的写入请求到内核
GIOStatus g_io_channel_flush (GIOChannel *channel, GError **error);
如果需要,可以完全关闭缓冲
g_io_channel_set_encoding (gio, NULL, NULL); g_io_channel_set_buffered (gio, FALSE);
第二个函数禁用缓冲。第一个函数将 IO 通道的编码设置为 NULL,这是禁用缓冲所必需的。
在我们的示例程序中,我们使用 g_io_channel_write_chars() 将一个小字符串写入管道。我们没有处理 IO 通道的缓冲。
当写入或读取文件时,Glib 会自动更新应用程序在文件中的位置。首次打开文件时,位置设置为开头。读取四个字节,然后位置随后设置为第五个字节。这是几乎所有开发人员都熟悉的文件 I/O 行为。
但是,有时应用程序希望自己查找文件。为此,Glib 提供了一个函数
GIOStatus g_io_channel_seek_position (GIOChannel *channel, gint64 offset, GSeekType type, GError **error);
成功调用将根据 offset 和 type 描述更改文件中的当前位置。
type 参数是 G_SEEK_CUR、G_SEEK_SET 或 G_SEEK_END 之一。G_SEEK_CUR 要求将文件的位置更新为距当前位置 offset 字节。G_SEEK_SET 要求将文件的位置设置为 offset。因此,以下代码将文件位置设置为文件开头
GIOStatus ret; GError err; ret = g_io_channel_seek_position (gio, 0, G_SEEK_SET, &err); if (ret) g_error ("Error seeking: %s\n", err->message);
最后,G_SEEK_END 要求将文件的位置设置为距文件末尾 offset 字节。
我们在示例应用程序中未使用 g_io_channel_seek_position(),因为即使我们有理由这样做,管道也是不可查找的。
完成 IO 通道后,它将被销毁,并且通过调用 g_io_channel_shutdown 关闭文件
GIOStatus g_io_channel_shutdown (GIOChannel *channel, gboolean flush, GError **err);
如果 flush 为 TRUE,则首先刷新任何挂起的 I/O。
Glib 提供了几个较旧的 IO 通道函数:g_io_channel_read()、g_io_channel_write() 和 g_io_channel_seek()。本文讨论的函数已取代这些较旧的、已弃用的函数,应改为使用这些函数。特别是,永远不要在同一 IO 通道上混合使用这些旧函数和其他函数。
我们现在已经涵盖了列表 1 中使用的所有 IO 通道接口。只有两个细节尚未解释。首先,在我们的示例程序中,我们以通常的方式创建管道
int fd[2], ret; ret = pipe (fd); if (ret) g_error ("Creating pipe failed: %s\n", strerror (errno));
其次,我们的 main() 函数初始化 Glib 主循环,调用我们的函数来初始化 IO 通道,然后运行主循环
int main (void) { GMainLoop *loop; loop = g_main_loop_new (NULL, FALSE); init_channels (); g_main_loop_run (loop); /* Wheee! */ return 0; }
g_main_loop_new() 函数创建一个新的主循环,并返回指向新的 GMainLoop 结构的指针。准备好开始运行主循环后,我们调用 g_main_loop_run() 并让 Glib 处理其余部分。
使用 IO 通道的应用程序将可移植的多路复用 I/O 解决方案与智能缓冲和 Glib 主循环集成相结合。结果是一个允许应用程序在数百个文件描述符之间处理 I/O 的解决方案。图形网络客户端可以管理其所有打开的套接字,无缝处理新连接,处理十几个打开的文件,并从单个位置和单个线程响应大量 GUI 事件。
Glib 的 IO 通道使 I/O 变得简单、高效,甚至——天哪——有趣!
本文资源: /article/8632。
Robert Love 是 Novell Ximian 桌面组的高级内核黑客,也是 Linux 内核开发(SAMS 2005)的作者,该书现已出第二版。他拥有佛罗里达大学的 CS 和数学学位。Robert 住在马萨诸塞州剑桥市。