Linux下C语言编程中的一些经验小结


=Start=

缘由:

总结、提高需要

正文:

参考解答:

1、在使用一个返回类型为指针的函数时,一定要明确知道是否需要在使用后free,因为不free会造成内存泄露,但不需要free却又主动free的话会造成程序异常且不容易检测;比如get_current_dir_name()函数返回的指针就需要主动free(),在它的手册中提到它是malloc()后返回的,需要调用者对非NULL返回调用free(),但是ttyname()返回的指针就不应该手动free(),因为ttyname()返回的是一个静态分配的字符串;

2、尽量通过传参的方式获取处理结果,避免通过函数内部malloc后返回指针的方式,一来是避免调用者忘记free导致的内存泄露,或者主动free导致的程序异常,二来是这样编写的函数是可重入且线程安全的;比如 ttyname() 和 ttyname_r() 中就应该优先使用ttyname_r();

3、关键路径上使用的函数最好要是异步的,避免成为瓶颈从而引发故障;比如:syslog()函数其实是同步而非异步的,具体信息可参见「rsyslog使用之坑」;

4、不要相信传入函数的参数,函数中的第一步操作就应该是参数的合理/合法性校验,不通过的话直接return,既高效又可避免错误;比如下面这个函数:

int get_cmdline_from_pid (int pid, char cmdline[PATH_MAX])
{
    char file[PATH_MAX];
    snprintf(file, PATH_MAX, "/proc/%d/cmdline", pid);
    int i, r = 0, fd = open(file, O_RDONLY);
    memset(cmdline, 0, PATH_MAX);
    if (fd > 0) {
        r = read(fd, cmdline, PATH_MAX);
        close(fd);

        for (i = 0; r > 0 && i < r-1; i++) {
            if (cmdline[i] == 0)
                cmdline[i] = ' ';
        }
        return 0;
    }
    return -1;
}

存在几个问题:1.现有的两个参数应该显式改为三个参数(因为C语言中的函数参数是没有数组这一类型的,它对于函数来说就是一指针,指针指向的原始数组大小需要用另一个参数来显式说明);2.函数体中第一步应该做参数的合理性校验,避免后面出现对NULL指针进行操作导致segmentation fault,同时也可以提高效率;3.对于系统调用read()的返回值没有做判断以及相应的操作,这样虽然对cmdline的内容没什么影响,但是函数的返回值却会是0,可能会造成调用者的误解并引发后续错误。修改后的效果如下:

int get_cmdline_from_pid (int pid, char cmdline[], int cmdline_size)
{
    if (pid <= 0 || cmdline == NULL || cmdline_size <= 0) {
        return -1;
    }

    char file[PATH_MAX];
    snprintf(file, PATH_MAX, "/proc/%d/cmdline", pid);
    int i, r = 0, fd = open(file, O_RDONLY);
    if (fd > 0) {
        memset(cmdline, 0, cmdline_size);
        r = read(fd, cmdline, cmdline_size);
        if (r <= 0) {
            close(fd);
            return -1;
        }

        for (i = 0; r > 0 && i < r-1; i++) {
            if (cmdline[i] == 0)
                cmdline[i] = ' ';
        }
        close(fd);
        return 0;
    }
    return -1;
}

增加了一些检查、判断之后,整体逻辑要清晰了不少,也可避免不少不必要的错误。

补充一个memset()对NULL指针进行操作导致「段错误」的示例:

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    // char *cwd = get_current_dir_name();
    // printf("current_dir_name = %s\n", cwd);
    // if (cwd) {
    //     free(cwd);    // we should free
    // }

    char *ttyname_b = ttyname (0);
    printf("ttyname = %s\n", ttyname_b);
    if (ttyname_b) {
        free(ttyname_b);    // should not free
    }

    char arr[100] = "this is an char array";
    printf("arr = %s\n", arr);
    memset(arr, 0, sizeof(arr));
    printf("arr = %s\n", arr);

    char *str = NULL;
    printf("str = %s\n", str);
    memset(str, 0, sizeof(str));    // segmentation fault
    printf("str = %s\n", str);

    return 0;
}

 

参考链接:

=END=

,

《 “Linux下C语言编程中的一些经验小结” 》 有 11 条评论

  1. Linux下如何为C语言项目添加合适的log机制

    深入理解log机制
    http://feihu.me/blog/2014/insight-into-log/

    How to implement a leveled debug system?
    https://stackoverflow.com/questions/2181451/how-to-implement-a-leveled-debug-system

    如何在日志文件中引入日期和时间信息(How to introduce date and time in log file) #简单易用(不过只是简单的printf,还没有写文件的功能)
    https://stackoverflow.com/questions/7411301/how-to-introduce-date-and-time-in-log-file

    [C++]How to implement a good debug/logging feature in a project
    https://stackoverflow.com/questions/6168107/how-to-implement-a-good-debug-logging-feature-in-a-project
    http://logging.apache.org/log4cxx/latest_stable/index.html

    Super fast C++ logging library.
    https://github.com/gabime/spdlog

    https://codingfreak.blogspot.com/2010/08/printing-logs-based-on-log-levels-in-c.html

  2. 深入理解可重入与线程安全
    https://blog.csdn.net/feiyinzilgd/article/details/5811157
    `
    如果一个函数能够安全的同时被多个线程调用而得到正确的结果,那么,我们说这个函数是线程安全的。所谓安全,一切可能导致结果不正确的因素都是不安全的调用。

    线程安全,是针对多线程而言的。那么和可重入联系起来,我们可以断定,可重入函数必定是线程安全的,但是线程安全的,不一定是可重入的。不可重入函数,函数调用结果不具有可再现性,可以通过互斥锁等机制,使之能安全的同时被多个线程调用,那么,这个不可重入函数就是转换成了线程安全。

    线程安全,描述的是函数能同时被多个线程安全的调用,并不要求调用函数的结果具有可再现性。也就是说,多个线程同时调用该函数,允许出现互相影响的情况,这种情况的出现需要某些机制比如互斥锁来支持,使之安全。

    ==

    可重入函数,描述的是函数被多次调用但是结果具有可再现性。

    为了保证函数是可重入的,需要做到一下几点:
    1,不在函数内部使用静态或者全局数据
    2,不返回静态或者全局数据,所有的数据都由函数调用者提供
    3,使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
    4, 如果必须访问全局数据,使用互斥锁来保护
    5,不调用不可重入函数
    `

  3. 如何写好 C main 函数
    https://zhuanlan.zhihu.com/p/68427001
    `
    main 函数是唯一的。
    main() 函数是开始执行时所执行的程序的第一个函数,但不是第一个执行的函数。第一个函数是 _start(),它通常由 C 运行库提供,在编译程序时自动链入。此细节高度依赖于操作系统和编译器工具链,所以我假装没有提到它。

    main() 函数有两个参数,通常称为 argc 和 argv,并返回一个有符号整数。大多数 Unix 环境都希望程序在成功时返回 0(零),失败时返回 -1(负一)。

    参数名称描述argc参数个数参数向量的个数argv参数向量字符指针数组

    参数向量 argv 是调用你的程序的命令行的标记化表示形式。

    当我从头开始编写 main.c 时,它的结构通常如下:

    /* main.c */
    /* 0 版权/许可证 */
    /* 1 包含 */
    /* 2 定义 */
    /* 3 外部声明 */
    /* 4 类型定义 */
    /* 5 全局变量声明 */
    /* 6 函数原型 */

    int main(int argc, char *argv[]) {
    /* 7 命令行解析 */
    }

    /* 8 函数声明 */
    `

  4. 为什么系统调用会消耗较多资源
    https://draveness.me/whys-the-design-syscall-overhead/
    `
    系统调用是计算机程序在执行的过程中向操作系统内核申请服务的方法,这可能包含硬件相关的服务、新进程的创建和执行以及进程调度,对操作系统稍微有一些了解的人都知道 — 系统调用为用户程序提供了操作系统的接口。

    C 语言的著名的 glibc 封装了操作系统提供的系统调用并提供了定义良好的接口,工程师可以直接使用器中封装好的函数开发上层的应用程序,其他编程语言的标准库也会封装系统调用,它们对外提供语言原生的接口,内部使用汇编语言触发系统调用。

    多数编程语言的函数调用只需要分配新的栈空间、向寄存器写入参数并执行 CALL 汇编指令跳转到目标地址执行函数,在函数返回时通过栈或者寄存器返回参数。与函数调用相比,系统调用会消耗更多的资源,如下图所示,使用 SYSCALL 指定执行系统调用消耗的时间是 C 函数调用的几十倍:

    Linux 执行系统调用的三种方法:
    1. 使用 软件中断(Software interrupt)触发系统调用;
    2. 使用 SYSCALL / SYSENTER 等汇编指令触发系统调用;
    3. 使用 虚拟动态共享对象(virtual dynamic shared object、vDSO)执行系统调用;

    虚拟动态共享对象(virtual dynamic shared object、vDSO)是 Linux 内核对用户空间暴露内核空间部分函数的一种机制,简单来说,我们将 Linux 内核中不涉及安全的系统调用直接映射到用户空间,这样用户空间中的应用程序在调用这些函数时就不需要切换到内核态以减少性能上的损失。

    当我们在编写应用程序时,系统调用并不是一个离我们很远的概念,一个简单的 Hello World 会在执行时触发几十次系统调用,而在线上出现性能问题时,可能也需要我们与系统调用打交道。虽然程序中的系统调用非常频繁,但是与普通的函数调用相比,它会带来明显地额外开销:

    * 使用软件中断触发的系统调用需要保存堆栈和返回地址等信息,还要在中断描述表中查找系统调用的响应函数,虽然多数的操作系统不会使用 INT 0x80 触发系统调用,但是在一些特殊场景下,我们仍然需要利用这一古老的技术;
    * 使用汇编指令 SYSCALL / SYSENTER 执行系统调用是今天最常见的方法,作为专门为系统调用打造的指令,它们可以省去一些不必要的步骤,降低系统调用的开销;
    * 使用 vSDO 执行系统调用是操作系统为我们提供的最快路径,该方式可以将系统调用的开销与函数调用拉平,不过因为将内核态的系统调用映射到『用户态』确实存在安全风险,所以操作系统也仅会放开有限的系统调用;

    应用程序能够完成的工作相当有限,我们需要使用操作系统提供的服务才能编写功能丰富的用户程序。系统调用作为操作系统提供的接口,它与底层的硬件关系十分紧密,因为硬件的种类繁杂,所以不同架构要使用不同的指令,随着内核的快速演进,想要找到准确的资料也非常困难,不过了解不同系统调用的实现原理对我们认识操作系统也有很大的帮助。
    `

  5. C语言为什么只需要include就能使用里面声明的函数?
    https://www.zhihu.com/question/389126944
    `
    这些问题其实是很多初学计算机编程语言的人的共同问题,有这样的问题其实很正常,包括我自己当年也是抱着这样的疑问学下去的。刚学C语言时一方面忙于记住和熟悉它的各种语法,一方面也对很多我不能理解的“常规用法”所困惑,其中就包括为什么include一个头文件就可以使用里面的函数,头文件里明明没有函数实现。带着这样的疑问我学完了C语言的所有基本语法,然后转入更深层次的学习。这时候我学到了程序在系统上运行的原理,编译器如何将一个源程序一步一步处理成可执行文件,以及链接和装载静态/动态库是怎么回事等等。

    问这个问题的人显然也是和我当年一样的初学者,所以不要担心,按照正规的计算机知识学习道路学习下去,到时候很多疑问就会自然解开。很多时候我们只是被比自己维度高的问题困扰而已,这时候我们需要做的就是先努力提升自己到能理解这个问题的维度,再去尝试学习和理解问题。因为这个问题问得都是课本上的内容,我并不能保证自己比优秀课本讲的还好,所以我下面简单回答一下题目中的两个问题,目的在于抛砖引玉,引导读到这里的人知道下面自己该去学什么。

    最后再次声明,本回答旨在抛砖引玉,要学习正统的关于程序运行的知识,请自行阅读优秀的计算机教材,市面上有很多,且很容易获得。不要在自己还完全没有概念的情况下,仅凭一两篇博客就结束了对这方面知识的片面理解。我上面只浅尝辄止的解释到了编译器在整个编译过程中怎么从头文件声明的函数名找到标准库函数,但是我并没有继续解释这些动态库是怎么被编译、装载和执行的。这些问题就留作思考,让还没有学过的朋友们带着问题从学习中寻找自己的答案吧:)
    `

  6. C语言发展史的点点滴滴
    https://mp.weixin.qq.com/s/rJVEKjxIrfiV-nSluvXZVg
    `
    一直想写一篇关于C语言的文章,里面包含C语言的发展史、创始人等相关事迹。但是却迟迟未写,主要原因是因为:在我看来,这个语言太过于伟大、耀眼。作为一个仅仅使用过C语言的普通开发来说,完全没资格去写。但是,最近在看过一篇丹尼斯.里奇写的《C语言发展史》之后,坚定了我写这篇文章的决心。不是歌功颂德,仅仅是以一种客观的视角去欣赏。

    1. C语言发展史
    1.1 C语言有多伟大
    1.2 C语言之父
    1.3 C语言的先辈
    1.3.1 BCPL语言之父
    1.3.2 B语言之父
    1.3.3 一组Ken与Dennis的照片
    1.4 C语言时间线
    1.5 unix时间线
    1.5.1 PDP-Unix
    1.5.2 First Edition Unix
    1.5.3 Second Edition Unix
    1.5.4 Unix与C语言
    1.6 第一个C语言编译器是怎样编写的?

    2. BCPL、B、C语言比较
    2.1 3种语言代码示例
    2.1.1 BCPL语言示例
    2.1.2 B语言示例
    2.1.3 C语言示例
    2.2 示例代码中三者的区别

    3. 历史为什么选择C语言
    1960s年代后期,贝尔实验室对计算机系统的研究进入繁盛时期。MIT、General Electric、Bell实验室合作的Mutlics项目以失败而告终(1969年左右)。就是在这个时期,Ken Tompson开始写Mutlics的替代品,他希望按照自己的设计构造一个令人舒服的计算系统(也就是Unix)。后来在写出第一个版本的Unix时,觉得Unix上需要一个新的系统编程语言,他创造了一个B语言。B语言是没有类型的C,准确说B语言是Tompson把BCPL挤进8K内存,被其个人大脑过滤后的产生的语言。
    由于B语言存在的一些问题,导致其只是被用来写一些命令工具使用。恰好在这个时期,Ritchie在B语言的基础上,进行了重新的设计改良,从而诞生了C语言。
    1973年,C语言基本上已经完备,从语言和编译器层面已经足够让Tompson和Ritchie使用C语言重写Unix内核。后来,Unix在一些研究机构、大学、政府机关开始慢慢流行起来,进而带动了C语言的发展。
    1978年,K&R编写的《The C Programming Language》出版,进一步推动了C语言的普及。
    用一句话总结就是:对的时间、对的地点,出现了对的人以及工具 (Unix与C语言的关系,有点像GNU与Linux kernel的关系,都是互相成就)。

    4. 标准C库及代码
    4.1 标准C库
    4.2 linux/lib/string.c

    5. 廉颇老矣, 尚能饭否?
    个人想说的是,只要计算机还是基于冯诺依曼体系结构,芯片还是基于物理制程。那么,都会有一片C的天空。因为,她知道一个最接近天空的地方(C是最接近汇编、机器语言的高级语言之一)。
    `

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注