You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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. 该问题的原因是什么?
  2. 如何解决?
  3. 该如何排查此类问题?

问题分析与解决

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 07:37:58