8

C语言陷阱与技巧第3节,怎样主动让出CPU?如何为C语言函数增加超时检测功能

 3 years ago
source link: https://blog.popkx.com/c-language-traps-and-skills-section-3-how-to-actively-release-cpu-how-to-increase-timeout-detection-function-for-c-language-func/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在Linux C语言程序开发中,这个场景经常出现:进程 A 负责驱动数据采集装置获取数据,进程 B 则负责接收数据并处理。显然,进程 B 需要等待进程 A 将一次数据采集完毕才可以进行下一步工作,因此约定进程 A 采集一次数据完毕时,将 ready 位由 0 置 1,进程 B 监测 ready 位,若发现数据采集完毕,就开始数据处理。

f2b387fff01dddb1924a71a0240589a9.png

适时地让出 CPU

如果数据到达之前,进程 B 的工作无法展开,那进程 B 显然应该时刻监测 ready 位由 0 变为 1,这样才能在数据准备完毕的第一时间开始处理数据的工作,这样的需求可以用类似下面这样的C语言代码解决,请看:

while(!ready);
/** 下一步工作 */

上面的C语言代码使用 while 循环不断检测 ready 位,一旦发现 ready 位被置 1,就立刻进行下一步的工作。这么做似乎绝对不会浪费一点点时间,因此是最好的做法。真的是这样吗?

如果整个系统只有进程 A 和进程 B,这么做的确很好。遗憾的是,如果系统里还有其他进程,上面这种做法就不太合适了,C语言程序遇到 while(!ready); 时,CPU 除了判断 ready 的值,其他什么都不做,这时,整个系统的效率就被拖累了。

虽然现代操作系统大都拥有“抢占式”的内核,但一般都是通过时间片轮转调度的,在 Linux 中,调度程序的周期一般在 10ms 量级。10ms 的时间,已经够 CPU 处理很多事情了。

7df5da4809835689c3a85db00da77342.png

如果数据的处理工作对时间的要求没有苛刻到连 10ms 都不能浪费,那么进程 B 在发现 ready 位没有被置位时,主动的将 CPU 让出给其他进程使用,显然可以提升整个系统的效率。因此,进程 B 等待 ready 置位的 C语言代码,按照下面这样写更加合适,请看:
while(!ready)
    usleep(10);
/** 下一步工作 */

usleep(10) 可以让进程 B 进入睡眠 10 us(微秒),但在 Linux 中,这么做能够使进程 B 将 CPU 暂时让出,交给操作系统分配给其他进程使用,进程 B 损失一点点时间效率,换来整个系统的效率提升。

超时判断,以及 usleep 的陷阱

如果进程 A 因为某种原因退出了,那 ready 位永远都不会被置 1,这将导致进程 B 卡死在下面这两行C语言代码:

while(!ready)
    usleep(10);
8079eb4dc6a2e3858c303dfebcf7fc7f.png

避免这种情况的一个好方法是增加超时判断,如果超过一段时间,进程 B 发现 ready 仍然没有被置位,就结束等待。要是进程 A 采集数据的周期是 t 秒,那超时时间就可以设置为 t+1 秒,这样就不会在进程 A 意外退出时,进程 B 仍然傻傻的等待 ready 位了。

不过,C语言并没有提供“超时”语法,这就需要程序员自己实现超时等待功能了。这种功能也并不难实现,假设超时时间为 5 秒,那相关的C语言代码可以如下实现:

int cnt = 50000;
while(!ready && cnt--)
    usleep(100);

使用上面这段C语言代码实现“超时等待”功能足够简单,但是可靠吗?假设 ready 位始终为 0,那上面这几行C语言代码就相当于下面这几行:

int cnt = 50000;
while(!ready && cnt--)
    usleep(100);
721f8a1e9987ec6ea71e0948293ca151.png

现在在 main() 函数中测试上述方法的实际睡眠时间,相关C语言代码如下,请看:
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

long get_cur_ms()
{
     struct timeval tv;
     gettimeofday(&tv, NULL);
     return tv.tv_sec*1000 + tv.tv_usec/1000;
}   
int main()
{
     long otime = get_cur_ms();
     printf("cur ms: %ld\n", otime);

     int cnt = 50000;
     while(cnt--)
         usleep(100);

     printf("err ms: %ld\n", get_cur_ms()-otime);

return 0;
}
0bdf0a656852bf55d0d623ac397ad4c3.png

上述C语言代码中的 get_cur_ms() 函数可以获得当前时间的毫秒数,在测试“超时”代码之前,先使用 otime 记录当前时间 ms 数,然后在使用“超时”代码后的时间 ms 数减去 otime,就可以得到“超时”代码实际的等待时间了。编译这段C语言代码并执行,得到如下结果:
# ./a.out 
cur ms: 1553603227960
err ms: 8406

根据“超时”C语言代码段,预期时间差输出应该是 5 秒,也即 err ms 应该等于 5000,但是输出却是 8406,与预期相差 68%, 这是怎么回事呢?其实查看 usleep 的使用说明就可以得到答案了:

7d233974628db2ead2a78e6f2b1e0bd3.png

根据上述文档说明,usleep(usec) 会将线程挂起至少 usec 秒,而不是严格的 usec 秒,具体多少时间则取决于系统的调度。如果系统比较繁忙,那么 usleep(usec) 的实际睡眠时间可能会被延长,这就解释了预期睡眠 5 秒的C语言代码,最终却睡眠了 8 秒多。

那“超时”代码该怎么写呢?应该明白 gettimeofday() 函数可以获取微秒量级的时间,用于“超时”估计是非常合适的,请看下面的C语言代码:

 while(get_cur_ms()-otime < 5000)
         usleep(100); 
d57f13464e46db866b00002ad77dd0b2.png

编译上述C语言代码并执行,可以得到如下输出:
# ./a.out 
cur ms: 1553608462083
err ms: 5000

可以看出,以上C语言代码的确提供了更加精确的“超时”功能。

感兴趣的读者可以自己掐秒表测试这两种“超时”C语言代码。

测试自定义的超时功能

请看下面的C语言代码:

 #include <stdio.h>                          
 #include <sys/time.h>                       
 #include <unistd.h>                         
 #include <pthread.h>                        

 int ready = 0;                              

 long get_cur_ms()                           
 {           
     struct timeval tv;                      

     gettimeofday(&tv, NULL);                

     return tv.tv_sec*1000 + tv.tv_usec/1000;
 }           

 void *thread(void *p)                       
 {           
     sleep(3);                               
     ready = 1;                              
     return NULL;                            
 }           

 int main()  
 {           
     pthread_t pid;                          
     pthread_create(&pid, NULL, thread, NULL);

     long otime = get_cur_ms();              

     while( !ready && get_cur_ms()-otime < 5000)
         usleep(100);                        
     if(get_cur_ms()-otime >= 5000)           
         printf("time out\n");               

     printf("err ms: %ld\n", get_cur_ms()-otime);

    return 0;
 }
8e15d05d8a56df6d0a1371ea088ae553.png

上述C语言代码使用 thread() 线程函数模拟进程 A 对 ready 标志位 3 秒后置 1,main() 函数若 5 秒仍未检测到 ready 被置 1,就提示 “time out”。编译上述C语言代码并执行,得到如下输出:
# gcc t.c -lpthread
# ./a.out 
err ms: 3000

因为 thread() 函数 3 秒后将 ready 置 1了,所以没有超时。现在将 thread() 中的 sleep(3) 改为 sleep(6),编译修改后的C语言代码并执行,得到如下输出:

13b9c701edb3c097c3be270e6802dea3.png

一切与预期一致。

从本节可知,在 Linux C语言程序开发中,需要使用 while 循环不断检查某标志位是否置位时,若对时间的要求不是特别苛刻,可以适当调用 usleep() 函数主动让出 CPU,以提升系统的整体效率。另外,若需要增加超时功能,使用 usleep() 函数误差常常会比较大,应该借助别的方法实现。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK