KEEP K.I.S.S.

tk's blog

setjmp 和 longjmp

本文主要讲解 C 库中的函数 setjmp 和 longjmp,也就是所谓的 非局部跳转。

本文主要翻译和出自 Jim Plank 的讲座 CS360 Lecture notes -- Setjmp

翻译 by tisyang 自我感觉不直观的翻译都在括号中附加了原文


setjmp()/longjmp()

Setjmp() 和 longjmp() 是在 C/Unix 下用于执行复杂控制流的子程序。

理解 setjmp() 和 longjmp() 的关键之一就在于理解 机器布局(machine layout), 这个在几周前的 汇编和内存分配(assembler and malloc) 讲座中描述过。 一个程序的状态完全取决于它内存中的内容(contents of its memory) (比如 代码(code), 全局变量(globals), 堆(heap), 和栈(stack)), 以及寄存器中的内容。 寄存器中的内容包括 栈指针寄存器(stack pointer,缩写 sp)、帧指针寄存器(frame pointer,指向栈中一个函数的local 变量的首地址,缩写 fp)和 程序计数器寄存器(program counter,缩写 pc)。 setjmp() 所做的事情就是保存当前这些寄存器的内容以便在以后的某个时刻 longjmp() 可以恢复它们。 因此,longjmp() 可以“返回” 到 setjmp()被调用时刻的程序的状态。

具体来看:

#include < setjmp.h >
int setjmp(jmp_buf env);

这是将当前寄存器的状态保存到 env 中。 如果打开 /usr/include/setjmp.h, 你将看到 jmp_buf 的定义如下:

#define _JBLEN  9
typedef struct { int _jb[_JBLEN + 1]; } jmp_buf[1];

这说明 jmp_buf 是一个包含数量为 _JBLEN+1 的整数数组。

因此, 当调用 setjmp() 时,传递的参数是这样一个整数数组的地址,接着函数把所有寄存器的值保存到这个数组之中。在这种情况下调用, setjmp() 会返回 0。

longjmp(jmp_buf env, int val);

Longjmp() 会将寄存器重置为保存在 env 中的值 ,这包括 spfp 以及 pc这意味着 longjmp() 函数不会返回。 相反, 当调用 longjmp() 时,程序流会返回到好像刚刚调用完 setjmp() (保存了 env )一样。 这是因为 pc(程序计数器)和其他寄存器的内容都被一起恢复了。此时,setjmp() 会返回传递给 longjmp() 的参数 val 的值,注意 val 的值不允许为 0 (请参阅 man 帮助系统)。因此,当 longjmp() 被调用时,setjmp() 会返回一个非0值, 程序流也从 setjmp() 中返回。

用一个示例来说明,看如下的代码 (in sj1.c):

Ruby 中的 Array Shuffle 算法

其实就是数组的洗牌算法,洗牌算法主要是生成一组互不相同的伪随机数列,可用于随机排序等等。

如果单纯依赖 C 库的 rand 去生成随机数来做的话,容易发生碰撞,尤其在需要生成的数列个数比较大的情况下。

Ruby 的 Array 有一个 shuffle 的方法,用于生成随机排序的元素新数组:

(1..12).to_a.shuffle 

=> [12, 9, 6, 5, 3, 1, 8, 4, 11, 7, 10, 2]

其实现如下:

static VALUE
rb_ary_shuffle_bang(VALUE ary)
{
    VALUE *ptr;
    long i = RARRAY_LEN(ary);
    rb_ary_modify(ary);
    ptr = RARRAY_PTR(ary);
    while (i) {
        long j = (long)(rb_genrand_real()*i);
        VALUE tmp = ptr[--i];
        ptr[i] = ptr[j];
        ptr[j] = tmp;
    }
    return ary;
} 

很简单,用 C 写个示例如下:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define ARRAY_SIZE(a)  ((sizeof(a) / sizeof(*(a))))

static void
shuffle(int *arr, size_t len)
{
    int i;
    i = len;
    while (i) {
        size_t j = (size_t)(rand() % i);
        i--;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;       
    }
}

int main(int argc, char *argv[])
{
    int array[] = {
        1, 2, 3, 4, 5, 6,
        7, 8, 9, 10, 11, 12
    };
    srand(time(NULL));
    shuffle(array, ARRAY_SIZE(array));
    for (int i = 0; i < ARRAY_SIZE(array); i++) {
        printf("%d, ", array[i]);
    }
    printf("\n");
    return 0;
}

屏幕 IO 是很慢的

今天测试写的一个 lua 脚本,用于文件解析和输出报表,然后发现输出很慢,差不多要 400s 左右的时间,输出的行数越 10万行,总数据量 1.2MB。

然后发觉自己在运行命令里关掉了 stdout 的 buffer,但是开启后并没有多大的速度提升,然而,如果把输出重定向(cmd /c xxx > output.txt)后,速度有了质变,只需要 0.31s,其中 IO 只用了 0.17s。

然后我就 google 了一下,发现了这个解释,:http://stackoverflow.com/questions/3857052/why-is-printing-to-stdout-so-slow-can-it-be-sped-up(Why is printing to stdout so slow? Can it be sped up?)

大意就是 stdout(仅指屏幕 IO) 的实现里大多都是无缓冲或者小缓冲(行缓冲),所以大量的输出时候会很慢。

-------

多谢 依云兄的指正

C 语言中的位取反操作符 续

前一篇文章的问题,在我查阅了《The C Programming Language》(K&R) 第二版后,发现了如下的定义

在附录A 参考手册的 A.7.4 一元运算符 二进制反码运算符 中有如下表述

一元运算符 ~ 的操作数必须是整型,结果为操作数的二进制反码。在运算过程中需要对操作数进行整形提升。如果操作数为无符号类型,则结果为提升后的类型能够表示的最大值减去操作数的值而得到的结果值。如果操作数为带符号型,则结果的计算方式为:将提升后的操作数转换为相应的无符号类型,使用运算符 ~ 计算反码,再将结果转换为带符号的类型结果的类型为提升后的操作数类型

对于整形提升,在附录A 参考手册的 A6.1 整形提升 有如下表述

在一个表达式中,凡是可以使用整形的地方都可以使用带符号或无符号的字符、短整型或整型位字段,还可以使用枚举类型的对象。如果原始类型的所有值都可用 int 类型表示,则其值将被转换为 int 类型;否则将被转换为 unsigned int 类型。这一过程称为整形提升(integral promotion)。

请注意红色标注的句子,“结果的类型为提升后的操作数类型”,意味着 ~(char) 、 ~(short) 等的结果类型都是整型(int)。

C 语言中的位取反操作符

话不多说,先来看一段代码:

 

 1 #include <stdio.h>
 2 
 3 int main(int argc, char *argv[])
 4 {
 5         unsigned char uc = 0x1f;
 6         unsigned int ui = ~uc;
 7         unsigned long long ull = ~uc;
 8 
 9         unsigned char cc = ~uc;
10         printf("uc = %#x\n", uc);
11         printf("cc: ~uc = %#x\n", cc);
12         printf("ui: ~uc = %#x\n", ui);
13         printf("ull: ~uc = %#llx\n", ull);
14         return 0;
15 }

因为用到了 C99 标准的 long long 类型,所以编译时候需要添加 "-std=c99" 选项:

gcc -Wall -std=c99 -o file file.c

输出结果(x86 mingw32):

 

uc = 0x1f
cc: ~uc = 0xe0
ui: ~uc = 0xffffffe0
ull: ~uc = 0xffffffffffffffe0
 
这里显现出了 位取反 操作的结果特殊性,对于通常而言,我们以为一个 unsigned char 位取结果的类型也应该是 unsigned char ,也就是说当我们把这个结果赋给一个 unsigned int (或者 unsigned long long)类型的值时候,这个值也应该是 不会超过 unsigned char 的取值范围的。但是上述代码显示出并非这样的情况。
 
上述代码显示出把 unsigned char 位取反值赋给 unsigned int 变量时,除了低位字节是补码外,高位字节都是 0 的补码 1了。对于赋值给 unsigned long long 变量也是如此。
 
我个人理解的解释是,在 x86-32 的机器上,机器字长为 32,寄存器长度也是32,对于像 unsigned char 这样的 1 byte 的值,运算时会存储在寄存器中的低位字节,高位字节都为0,然后位取反操作指令会对整个寄存器的值位取反,然后赋值给 unsigned int (4 bytes)这样的变量时,是将整个寄存器值取出。这样高位字节都会是 0 的补码 1。但是这样怎么解释赋值给 unsigned long long 类型变量发生的情况呢?或者说对于位取反操作的结果是个特殊状态的值?
 
PS: 最近在看 CSAPP(《深入理解计算机系统》)第二版,其中在做练习题 2.12 发现了上述情形,之前不知道,所以就研究了一下下,不过还是半解。。。。