GTK3程序监听命名管道后首次触发g_io_add_watch()致CPU占满100%
问题描述
我开发了一个监听名为“info”的命名管道(named pipe)的GTK程序,通过管道传入的字符串更新标签文本,代码如下:
#include <gtk/gtk.h> GtkWidget* label; static void activate (GtkApplication *app, gpointer user_data) { GtkWidget *window; window = gtk_application_window_new (app); gtk_window_set_title (GTK_WINDOW (window), "Window"); gtk_window_set_default_size (GTK_WINDOW (window), 100, 20); // create label label = gtk_label_new ("aho"); gtk_container_add (GTK_CONTAINER (window), label); gtk_widget_show_all (window); } gboolean my_callback(GIOChannel *source, GIOCondition condition, gpointer data){ gchar *buf=NULL; GError *error = NULL; g_io_channel_read_line(source, &buf, NULL, NULL, NULL); gtk_label_set_text (GTK_LABEL(label), buf); g_free(buf); // for next watch g_io_add_watch(source, G_IO_IN,(GIOFunc) my_callback, NULL); return FALSE; } int main (int argc, char **argv) { GtkApplication *app; GIOChannel *channel; int status, fd; app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); channel = g_io_channel_new_file ("info", "r", NULL); g_io_add_watch(channel, G_IO_IN,(GIOFunc) my_callback, NULL); status = g_application_run (G_APPLICATION (app), argc, argv); g_object_unref (app); return status; }
程序运行正常,但当外部进程(如执行echo neko > info)向管道写入字符串后,首次触发g_io_add_watch()会导致CPU占用率达到100%。我使用gprof分析但未得到有效信息:
pi@raspberrypi:~/gtk3-sandbox $ gprof ./infobox3 gmon.out Flat profile: Each sample counts as 0.01 seconds. no time accumulated % cumulative self self total time seconds seconds calls Ts/call Ts/call name % the percentage of the total running time of the time program used by this function. cumulative a running sum of the number of seconds accounted seconds for by this function and those listed above it. self the number of seconds accounted for by this seconds function alone. This is the major sort for this listing. calls the number of times this function was invoked, if this function is profiled, else blank. self the average number of milliseconds spent in this ms/call function per call, if this function is profiled, else blank. total the average number of milliseconds spent in this ms/call function and its descendents per call, if this function is profiled, else blank. name the name of the function. This is the minor sort for this listing. The index shows the location of the function in the gprof listing. If the index is in parenthesis it shows where it would appear in the gprof listing if it were to be printed.
请问:
- 该问题的原因是什么?
- 如何解决?
- 该如何排查此类问题?
问题分析与解决
1. 问题原因
(1)命名管道EOF触发无限事件循环
外部进程执行echo neko > info后会立即关闭管道写入端,此时管道读端会收到EOF(文件结束符),g_io_channel_read_line会立即返回EOF状态,但代码未处理这种情况,导致事件循环持续触发G_IO_IN事件。
(2)错误的watch管理逻辑
回调函数每次执行完都调用g_io_add_watch添加新监听,同时返回FALSE——这会让GLib移除当前watch。但管道已处于EOF状态,新添加的watch会立即触发G_IO_IN,形成无限循环,直接占满CPU。
(3)忽略IO操作返回值
代码未检查g_io_channel_read_line的返回值,当读到EOF或出错时,buf可能为NULL,不仅会引发gtk_label_set_text的潜在错误,还会继续执行watch添加逻辑,加剧循环问题。
2. 解决方法
修改回调函数,正确处理IO状态、管道HUP事件,并调整watch管理逻辑:
#include <gtk/gtk.h> GtkWidget* label; static void activate (GtkApplication *app, gpointer user_data) { GtkWidget *window; window = gtk_application_window_new (app); gtk_window_set_title (GTK_WINDOW (window), "Window"); gtk_window_set_default_size (GTK_WINDOW (window), 100, 20); label = gtk_label_new ("aho"); gtk_container_add (GTK_CONTAINER (window), label); gtk_widget_show_all (window); } gboolean my_callback(GIOChannel *source, GIOCondition condition, gpointer data){ gchar *buf = NULL; GIOStatus status; GError *error = NULL; // 处理管道写入端关闭的HUP事件 if (condition & G_IO_HUP) { g_io_channel_shutdown(source, TRUE, NULL); return FALSE; // 移除watch,停止监听 } // 读取管道内容并检查状态 status = g_io_channel_read_line(source, &buf, NULL, NULL, &error); if (status != G_IO_STATUS_NORMAL) { if (error) { g_warning("读取管道错误: %s", error->message); g_error_free(error); } g_free(buf); return FALSE; // 出错或EOF时停止监听 } // 更新标签文本 gtk_label_set_text(GTK_LABEL(label), buf); g_free(buf); // 返回TRUE保留当前watch,无需重复添加 return TRUE; } int main (int argc, char **argv) { GtkApplication *app; GIOChannel *channel; int status; app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); channel = g_io_channel_new_file ("info", "r", NULL); // 只添加一次watch,回调返回TRUE会持续生效 g_io_add_watch(channel, G_IO_IN | G_IO_HUP, (GIOFunc) my_callback, NULL); status = g_application_run (G_APPLICATION (app), argc, argv); g_object_unref (app); g_io_channel_unref(channel); // 释放通道资源 return status; }
关键修改点:
- 监听
G_IO_HUP事件,捕获写入端关闭的情况,关闭管道并停止监听。 - 检查
g_io_channel_read_line的返回值,出错或读到EOF时停止监听。 - 回调返回
TRUE,保留原watch,无需每次重新添加。 - 释放
GIOChannel资源,避免内存泄漏。
3. 此类问题的排查思路
(1)换用适合事件循环的工具
gprof对GLib/GTK异步事件循环无效,改用以下工具:
- strace:执行
strace -p <程序PID>,观察程序反复调用的系统调用。若看到反复调用read且返回0(EOF),即可定位到管道读端的无限循环问题。 - gdb:在回调函数中设置断点,查看每次触发的
condition参数和g_io_channel_read_line的返回值,判断是否进入异常循环。
(2)添加调试日志
在回调函数中打印关键信息,比如:
g_print("触发条件: %d,读取状态: %d\n", condition, status);
通过日志直观查看是否出现频繁触发,以及IO操作的结果。
(3)熟悉GIOChannel工作机制
g_io_add_watch添加的watch,回调返回TRUE会持续生效,返回FALSE会被移除,无需手动重复添加。- 命名管道写入端关闭后,读端
read会立即返回EOF,G_IO_IN事件会持续触发,直到管道关闭或watch被移除。 - 必须检查所有IO操作的返回值,不能忽略错误和EOF情况。
内容的提问来源于stack exchange,提问作者Ueda Takeyuki
相关产品推荐
相关产品推荐

