函数指针

在 C 语言中,可以通过以下方式定义函数指针:

返回值类型 (*指针变量名)(参数列表)

其中,指针变量名 是自定义的指针变量名称, 返回值类型 表示该函数返回值的类型, 参数列表 表示该函数接受的参数类型及参数个数。

例如,假设我们有一个函数 add,它接受两个整数作为参数,返回它们的和。那么我们可以定义一个指向该函数的指针类型和对应的指针变量,如下所示:

int add(int a, int b)
{
    return a + b;
}

int (*p_add)(int, int);   // 定义函数指针类型和指针变量
p_add = &add;             // 将指针指向 add 函数

int result = (*p_add)(2, 3);  // 调用 p_add 指向的函数

在上述代码中,p_add 是一个指向函数的指针变量,它被定义为指向接受两个整数参数、返回整数类型的函数。通过将其指向 add 函数,并使用 (*p_add)(2, 3) 的方式调用函数,就可以得到 2 + 3 = 5 的结果。

除了上述方法外,还可以使用 typedef 来简化函数指针类型的定义。例如,可以使用以下方式定义一个函数指针类型 ADD_FUNC,并使用它来定义指针变量和调用函数:

typedef int (*ADD_FUNC)(int, int);  // 定义函数指针类型

int add(int a, int b)
{
    return a + b;
}

int main()
{
    ADD_FUNC p_add = &add;           // 定义指针变量
    int result = p_add(2, 3);        // 调用指针指向的函数
    return 0;
}

这种方式可以让代码更加简洁易读,提高可维护性。

阅读全文

宏与条件编译

C语言宏是一种类似于函数的预处理器指令,用于在代码中定义和使用常量、函数以及重复代码块。它们通过在代码中使用#define指令来定义,可以接受参数并生成文本替换。在编译时,预处理器会将所有宏都展开为它们的定义,然后进行编译。

条件编译

C语言中的条件编译指令包括 #if#ifdef#ifndef#elif#endif。这些指令可以根据预定义宏、常量或表达式的值来判断是否编译指定的代码块。

  • #if 指令用于条件编译,可以根据预定义的宏、常量或表达式的值来判断是否编译指定的代码块。其语法格式为:#if 常量表达式
  • #ifdef 指令用于检查某个宏是否已经被定义。如果该宏已经被定义,则编译指定的代码块。其语法格式为:#ifdef 宏名
  • #ifndef 指令与 #ifdef 相反,用于检查某个宏是否未被定义。如果该宏未被定义,则编译指定的代码块。其语法格式为:#ifndef 宏名
  • #elif 指令用于在多个条件之间进行选择。如果前面的条件不成立,则继续判断下一个条件。其语法格式为:#elif 常量表达式
  • #endif 指令用于结束条件编译的代码块。其语法格式为:#endif

在条件编译中,通常使用 #define 指令定义宏,并将它们传递给编译器。例如:

#define DEBUG 1     // 定义一个名为 DEBUG 的宏,值为1
#ifdef DEBUG
    printf("Debugging is enabled.\n");
#else
    printf("Debugging is disabled.\n");
#endif

上述代码中,如果 DEBUG 宏已经被定义,则输出 “Debugging is enabled.";否则输出 “Debugging is disabled."。

带参数的宏

带参数的宏是一种宏定义,可以接受一个或多个参数,并在使用该宏时将这些参数替换成指定的值。带参数的宏通常用于简化复杂的表达式,增强代码可读性和可维护性。

带参数的宏的基本语法格式为:

#define 宏名(参数列表) 替换文本

其中,宏名是自定义的宏名称,参数列表是用括号括起来的一组宏参数,多个参数之间用逗号分隔。替换文本是宏定义中要替换成的文本内容,可以包含参数名和其他字符。

例如,下面是一个简单的带参数的宏定义:

#define MIN(a, b) ((a) < (b) ? (a) : (b))

这个宏接受两个参数 ab,并比较它们的大小,返回最小值。在使用该宏时,可以像调用函数一样传入实际参数:

int x = 10, y = 20, min;
min = MIN(x, y);   // 返回10,等价于 min = ((x) < (y) ? (x) : (y));

当宏定义中涉及运算优先级时,加括号是非常重要的。以下是一个简单的例子,其中宏 MAX 比较两个值并返回其中较大的那个值:

#define MAX(a, b) (((a) > (b)) ? (a) : (b))

在这个宏定义中,使用了条件运算符 ?: 和比较运算符 >,而条件运算符的优先级高于比较运算符。为确保在使用此宏时不会出现错误结果,需要在所有操作符周围添加括号。

例如,假设有以下代码:

int x = 2, y = 3;
int z = MAX(x + 1, y - 1);

如果没有使用括号,则该代码将展开为:

int z = x + 1 > y - 1 ? x + 1 : y - 1;

由于条件运算符 ?: 的优先级高于比较运算符 >,因此 x + 1 和 y - 1 将首先被比较,然后根据比较结果选择一个分支,而 x + 1 和 y - 1 的加法和减法运算将被忽略。这可能导致得到错误的结果。

但是,在使用宏定义中添加括号之后,该代码将展开为:

int z = ((x + 1) > (y - 1)) ? (x + 1) : (y - 1);

这确保了加法和减法运算首先被执行,并且条件运算符按照预期的方式为其提供参数。

在编写宏定义时,正确地设置运算符的优先级和添加括号是至关重要的,以确保宏在使用时不会导致错误的结果。

阅读全文

Linux网络编程IO模型

IO

IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网络等)。

IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。

LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。

对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。

所以,对于一个网络输入操作通常包括两个不同阶段:

等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
从内核缓冲区复制数据到进程空间。

阻塞IO模型

进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程,操作成功则进程获取到数据。

非阻塞IO模型

进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞,进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

IO复用模型

进程的多个IO可以注册到一个复用器(select)上,然后调用该select, select会监听所有注册进来的IO。
如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞,而当任一IO在内核缓冲区中有可数据时,select调用就会返回,而后进程再次发起读取IO,读取内核中准备好的数据。

信号驱动IO模型

当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞,当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。

异步IO模型

当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结,内核把整个IO处理完后,会通知进程结果,如果IO操作成功则进程直接获取到数据。

IO模型比较

同步IO:导致请求进程阻塞,直到I/O操作完成。
异步IO:不导致请求进程阻塞。

阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型都为为同步IO,只有异步IO模型是异步IO。

阅读全文

使用树莓派搭建网站

整体架构

公网入口

树莓派服务暴露到公网,使用的方案是用Frp做内网穿透,在阿里云的公网ECS上搭建了Frp Server,树莓派上跑了Frp Client,实现将ECS的7050端口转发到树莓派的80端口上,在树莓派的80端口启动了Nginx,这样访问ECS的7050端口,就访问到了树莓派上的Nginx上。

ECS上同时托管了其他网站,统一用了Traefik做入口代理服务器监听在了ECS的80端口,便在Traefik上添加树莓派域名并配置反向代理到本地的7050端口,实现通过域名访问到树莓派的Nginx服务。

树莓派上的Nginx服务可以通过公网访问,之后在树莓派内启动的其他服务,都可以通过这个Nginx做转发暴露出去。

Web服务

server {
        listen 80 default_server;

        server_name raspberrypi.v95.xyz;

        client_max_body_size    10m;

        root /home/pi/www/public;

        index index.html index.htm;

        location ^~ /.git {
                return 403;
        }

        location ~ \.php {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/var/run/php/php-fpm.sock;
        }

        location ~ /api {
              proxy_pass http://127.0.0.1:5050;
        }
}

Nginx上配置public目录作为网站根目录,静态文件放到这个目录就可以,安装php-fpm配置fastcgi_pass实现同时支持执行php脚本。其他在树莓派上用python开发的api服务,使用gunicron跑在了本地的5050端口,加上配置api路径proxy_pass到本地的5050端口。

CDN与HTTPS

实测外部打开网站的所有请求都到树莓派上速度还可以,请求百毫秒左右差不多都可以响应,可见现在公网质量可以。但考虑树莓派的IO性能拉跨和节省ECS弹性IP流量,优化网站的访问体验,便在阿里云给树莓派域名配置了CDN同时申请了免费的SSL证书配置在CDN上,实现了网站访问加速和支持HTTPS.

阅读全文

树莓派的启动过程

计算机启动过程

以嵌入式芯片(MCU)为例,芯片的指令芯片厂商已经通过电路硬件实现在了芯片内部,自己实现的软件,要通过芯片对应的编译器(汇编、C语言)编译成该芯片可以识别的指令,最后写到芯片的Rom里。芯片上电后,CPU会自动去Rom里取指令加载到Ram然后不停的执行。

嵌入式芯片在一个芯片里把我们日常用的PC机的组件都做进去了,CPU,内存(Ram),硬盘(Rom),并且在CPU内固化好逻辑告诉CPU上电后到指定位置取用户存放的代码,放到内存指定开始位置,然后开始执行。

我们日常用的PC机,CPU、内存、硬盘都是分开的,硬盘系统不仅可以重复安装还可以格式化为不同文件系统装不同操作系统,哪里实现的加载逻辑呢? PC上电后CPU从哪里开始执行程序?

PC机的CPU也有这个固化逻辑从哪里取代码、放到内存哪个位置开始执行,只是第一个加载的程序不是用户硬盘中放的程序,而是主板的ROM中存放的系统程序(BIOS程序)。

CPU先取到BIOS的代码执行,BIOS程序完成系统检测等工作,然后按照用户的设置引导CPU接下来上那块硬盘,那个区,那个文件取代码,放到那个内存地址,最后从那个内存地址开始执行。

CPU执行BIOS程序沿着BIOS的引导在硬盘上找到程序放入0x7c00处开始的内存位置,然后跳转到0x7c00开始执行。注意这个程序通常还不是操作系统程序,而是我们常说的“引导”程序,比如GRUB。

CPU继续执行引导程序,沿着引导程序的引导加载操作系统代码到内存,并从某时刻跳转到操作系统main函数,至此BIOS和引导程序的使命完成,计算机内的天下全部变成操作系统的了。

相比之下:

MCU:Rom中的程序 = PC机:BIOS程序+引导程序+操作系统程序。

为什么PC机的启动这么复杂,其实还是计算机解耦分层的思想。PC上BIOS程序最简单但也比MCU的程序复杂多,要实现指引CPU上那块硬盘,硬盘分区是MBR还GPT的哪里找接下来的程序,让CPU能读硬盘BIOS还要实现支持硬盘分区的文件系统等等功能。接下来引导程序,作为中间层可以解耦BIOS和操作系统的绑定关系,相信大家学习Linux时候应该装过Windows和Linux双系统吧,开机时候是不是要在GRUB界面选择要启动的操作系统。

树莓派启动过程

了解了PC的启动过程,参考树莓派的文档,便很容易理解树莓派的启动过程。有意思的是,树莓派的启动过程不是CPU完成的,而是用的GPU,当然树莓派的CPU和GPU是集成在一起的。

首先,系统芯片加载固化程序(Boot Rom), 这个程序在支持的启动设备(SD卡、USB等)中寻找启动程序文件bootcode.bin,把bootcode.bin加载到Cache中并运行它,

然后,bootcode.bin程序会检索程序start.elf,然后运行它。

最后,start.elf程序将kernel.img加载到内存中,内核开始运行,系统启动完成。

其中需要注意,存放bootcode.bin、start.elf的分区需要是FAT32(所以明白了平时做的树莓派系统卡一个是FAT32分区,一个是Ext4),树莓派4因为内置了EEPROM芯片存放bootcode.bin程序,所以树莓派4会忽略bootcode.bin,直接加载EEPROM芯片中的代码。

另外start.elf程序支持用户配置config.txt,实现树莓派系统配置,同时支持配置加载kernel.img时的参数cmdline.txt。

当系统完全启动后,启动相关的文件和程序,可以在/boot目录下找到,用户可以修改config.txt和cmdline.txt然后重启树莓派配置一些初始化参数。

树莓派更新固件和内核

经过一番研究,顺便还发现,树莓派启动相关的代码,是不开源的,系统定制的kernel代码是开源的,bootcode.bin,start.elf官方放到Github上的都是二进制!要重新认识树莓派了,不完全称得上是开源硬件。

网上讨论,树莓派的芯片是博通专供且这部分固件代码也不开源,树莓派的自由与方便差不多是Kernel之上的自由和方便。基金会和博通这么干,也是市面上没有树莓派仿品板子的原因。

既然代码不开源,官方提供了更新这部分软件的通道,使用rpi-update命令可以更新官方编译好的kernel和/boot目录下的相关固件文件,树莓派4更新EEPROM可以使用rpi-eeprom-update命令。

阅读全文