一、问题

使用percpu变量时编译报错:

### make  in drv
make -C /usr/src/linux-wm M=/Packet/ac/module/saas_ctl/drv modules
  CC [M]  /Packet/ac/module/saas_ctl/drv/saas_action.o
  CC [M]  /Packet/ac/module/saas_ctl/drv/saas_main.o
/Packet/ac/module/saas_ctl/drv/saas_main.c: In function 'match_saas_rules':
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: 'per_cpu__g_ssl_cpu_data' undeclared (first use in this function)
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: (Each undeclared identifier is reported only once
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: for each function it appears in.)
cc1: warnings being treated as errors
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: type defaults to 'int' in declaration of 'type name'
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: invalid type argument of 'unary *' (have 'int')
make[3]: *** [/Packet/ac/module/saas_ctl/drv/saas_main.o] Error 1
make[2]: *** [_module_/Packet/ac/module/saas_ctl/drv] Error 2
make[1]: *** [all] Error 2
make: *** [drv] Error 2

原因:

percpu变量在其他模块定义的,当前模块使用前要声明:

DECLARE_PER_CPU(type, name);

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/add-two-numbers

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储一位数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

您可以假设除了数字0之外,这两个数都不会以0开头。

示例:

  • 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
  • 输出:7 -> 0 -> 8
  • 原因:342 + 465 = 807

二、题解

思路:

我们使用变量来跟踪进位,并从包含最低有效位的表头开始模拟逐位相加的过程。:

算法:

就像你在纸上计算两个数字的和那样,我们首先从最低有效位也就是列表l1l2的表头开始相加。由于每位数字都应当处[0, 9]的范围内,我们计算两个数字的和时可能会出现 “溢出”。例如,5 + 7 = 12。在这种情况下,我们会将当前位的数值设置为2,并将进位carry = 1带入下一次迭代。进位carry必定是0或1,这是因为两个数字相加(考虑到进位)可能出现的最大和为 9 + 9 + 1 = 19。

伪代码如下:

  • 将当前结点初始化为返回列表的哑结点。
  • 将进位carry初始化为 00。
  • 将p和q分别初始化为列表l1和l2的头部。
  • 遍历列表l1和l2直至到达它们的尾端。

    • 将x设为结点p的值。如果p已经到达l1的末尾,则将其值设置为0。
    • 将 y设为结点q的值。如果q已经到达 l2l2 的末尾,则将其值设置为0。
    • 设定sum = x + y + carry。
    • 更新进位的值,carry = sum / 10。
    • 创建一个数值为 (sum % 10) 的新结点,并将其设置为当前结点的下一个结点,然后将当前结点前进到下一个结点。
    • 同时,将 pp 和 qq 前进到下一个结点。
  • 检查carry = 1是否成立,如果成立,则向返回列表追加一个含有数字 11 的新结点。
  • 返回哑结点的下一个结点。

请注意,我们使用哑结点来简化代码。如果没有哑结点,则必须编写额外的条件语句来初始化表头的值。

特别注意以下情况:

  1. 当一个列表比另一个列表长时
  2. 当一个列表为空时,即出现空列表
  3. 求和运算最后可能出现额外的进位,这一点很容易被遗忘

三、代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode *rs, *p, *q, *cur;
        int carry = 0, sum, x, y;

        rs = new ListNode(0);
        cur = rs;

        p = l1;
        q = l2;

        // [1] 注意循环终止的条件是两个链表都不为空了才停止
        while (p || q) {
            // [2] 注意p或者q等于NULL的清情况
            x = p ? p->val : 0;
            y = q ? q->val : 0;

            // [3] 注意加上进位
            sum = x + y + carry;

            cur->next = new ListNode(sum % 10);
            carry = sum / 10;

            // [4] 注意处理p或q节点等于NULL的情况
            p = p ? p->next : NULL;
            q = q ? q->next : NULL;
            cur = cur->next;
        }

        // [5] 注意最后的进位
        if (carry) {
            cur->next = new ListNode(carry);
        }
        
        // [6] 返回rs的下一个节点
        return rs->next;
    }
};

复杂度分析:

  • 时间复杂度:O(max(m,n)),假设m和n分别表示l1和 l2的长度,上面的算法最多重复max(m,n)次。
  • 空间复杂度:O(max(m,n)), 新列表的长度最多为max(m,n)+1。

来源:力扣(LeetCode)

链接:581. 最短无序连续子数组
著作权归领扣网络所有。

一、题目描述

给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

你找到的子数组应是最短的,请输出它的长度。

示例 1:

输入: [2, 6, 4, 8, 10, 9, 15]

输出: 5

解释: 你只需要对 [6, 4, 8, 10, 9] > 进行升序排序,那么整个表都会变为升序排序。

说明 :

  1. 输入的数组长度范围在[1, 10,000]。
  2. 输入的数组可能包含重复元素,所以升序的意思是<=。

二、题解

官方解析

2.1 排序

算法分析

对数组进行排序,将未排序数组和已排序的数组对比。数组两端朝中间遍历,找到最左边和最右边的不匹配元素,中间的元素就是无序的数组范围。

代码:

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        int i, j;

        // 建立辅助数组
        vector<int> sortNums(nums);
        // 排序
        sort(sortNums.begin(), sortNums.end());

        // 找到
        for (i = 0; i < nums.size(); i++) {
            if (nums[i] != sortNums[i]) {
                break;
            }
        }

        // 考虑数组已全部有序的情况
        if (i == nums.size()) {
            return 0;
        }

        for (j = nums.size() - 1; j >= 0; j--) {
            if (nums[j] != sortNums[j]) {
                break;
            }
        }
        
        return j - i + 1;
    }
};

复杂度分析

时间复杂度:O(nlog(n)) 。排序消耗nlogn的时间。

空间复杂度:O(n) 。拷贝了一份原数组来进行排序。

2.2 不使用额外空间的用法

{ 2, 6, 4, 8, 10, 9, 15 }为例,该数组中,最短无序数组是{6, 4, 8, 10, 9},可以大概看出的规律是:区间的起点在原数组的第一个降序序列附近(6和4这里),区间的终点在从右到左第一个升序序列附近(10和9这里),因此这里可以认为无序子数组的起点和终点和整个数组的升降点相关。

那是不是说直接从第一个降序点和升序点取子数组就可以了呢?不行,以{ 3, 4, 6, 9, 2, 1 }为例,从左往右的第一个降序点是9和2,从右往左的升序点是1和2,但是失序的数组并不只是这三个元素{9, 2, 1},而是{3, 4, 6, 9, 2, 1}。此时从左往右看,元素9左边大于1的数组序列也都属于失序数组元素;从右往左看,元素2右边所有比9小的也属于失序数组。

因此能通过以下算法即便能找出失序数组:

  1. 从左往右找到第一个降序点,从这个点开始找到最大的元素max。
  2. 从右往左找到第一个升序点,从这个点开始找到最小的元素min。
  3. 从左往右遍历,第一个大于min的元素即是失序数组的起点。
  4. 从右往左遍历,第一个小于max的元素即是失序数组的终点。

代码:

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        int max = INT_MIN;
        int min = INT_MAX;

        int i, j;

        // 从左往右,出现降序之后找最大值
        for (i = 1; i < nums.size(); i++) {
            if (nums[i] < nums[i - 1]) {
                min = min < nums[i] ? min : nums[i];
            }
        }
        
        // 从右往左,出现升序之后找最小值
        for (j = nums.size() - 1; j > 0; j--) {
            if (nums[j - 1] > nums[j] ) {
                max = max > nums[j - 1] ? max : nums[j - 1];
            }
        }
        
        // 从左往右找到第一个大于min的元素
        for (i = 0; i < nums.size(); i++) {
            if (nums[i] > min)
                break;
        }
        
        // i遍历到最后一个元素,说明数组已经有序了
        if (i == nums.size())
            return 0;
        
        // 从右往左找到第一个小于max的元素
        for (j = nums.size() - 1; j >= 0; j--) {
            if (nums[j] < max)
                break;
        }
        // 计算区间大小
        return j - i + 1;
    }
};

算法复杂度

时间复杂度:O(n)。使用了4个O(n)的循环。

空间复杂度:O(1)。使用了常数空间。

一、class和struct的区别

C++中class和struct的区别:

  1. 继承权限,struct的默认继承权限为public,class的默认继承权限为private。
  2. 访问权限,struct的默认访问权限为public,class的默认访问权限为private。

网上还流传着其他一些的区别,但总体来说最大的区别就是这两点,其他的区别或许并不常用到。

二、C和C++中struct的区别

第一、C++中的struct可以定义成员函数,但是C语言不行,C语言中的结构体可以定义函数指针。

例如以下代码:

struct stu_st {
    void print();
};

int main() {
    return 0;
}

使用GCC编译会报错:

第二、C语言声明结构体必须要加struct,C++不用。

C语言中如果不加struct声明变量,编译器会报错:

如若不想加struct修饰,则需要使用typedef来重新定义类型。

第三、C语言中空结构体大小为0,C++中结构体大小为1。

相同的代码:

#include "stdio.h"

struct stu_st {
};

int main() {
    printf("%u\n", (unsigned int)sizeof(struct stu_st));
    return 0;
}

使用gccg++编译结果也不一样:

一、linux内核模块

Linux模块是一些可以作为独立程序来编译的函数和数据类型的集合。之所以提供模块机制,是因为Linux本身是一个单内核。单内核由于所有内容都集成在一起,效率很高,但可扩展性和可维护性相对较差,模块机制可弥补这一缺陷。

Linux模块可以通过静态或动态的方法加载到内核空间,静态加载是指在内核启动过程中加载;动态加载是指在内核运行的过程中随时加载。

一个模块被加载到内核中时,就成为内核代码的一部分。模块加载入系统时,系统修改内核中的符号表,将新加载的模块提供的资源和符号添加到内核符号表中,以便模块间的通信。

内核提供了接口函数来注册我们自己的模块:

// linux/init.h
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 * 
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)    __initcall(x);


/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 * 
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)    __exitcall(x);

module_initmodule_exit分别用于加载和卸载模块,其中的x的函数声明为:

int f(void);

二、编译一个自己的模块

2.1 编写一个模块

首先下载内核源码:yum groupinstall "Development Tools",、安装好后源码在/usr/src/kernels/2.6.32-754.9.1.el6.x86_64/注意最后面的一串内核版本,可能和uname -r不一致

然后准备测试代码test.c

#include <linux/module.h>
#include <linux/init.h>

static int test_init(void) {
    printk("===Test Module Start!===\n");
    return 0;
}

static int test_exit(void) {
    printk("===Test Module Has Been Closed!===");
    return 0;
}

module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("MaQian");

其中linux/module.hlinux/init.h是必须包含的两个问头文件,MODULE_LICENSE是模块许可证声明,一般设置为GPL,可选。MODULE_AUTHOR是模块作者,可选。

2.2 Makefile编写

# test是模块的名字
obj-m := test.o
# 内核代码的位置
KERNEL := /usr/src/kernels/2.6.32-754.9.1.el6.x86_64/
# 当前模块的目录
PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNEL) M=$(PWD) modules

.PHONY: clean
clean:
    rm -rf *.o *.ko

如果不出意外,执行make即可生成我们要的模块文件test.ko,但是一般情况下,第一次编译是会失败的,会报错,错误的解决方案在最下面。

2.3 挂载模块到系统

挂载驱动使用insmod,卸载使用rmmod,先开启另外一个窗口开始打印调试信息:

> while :; do dmesg -c; sleep 1; done

插入模块到系统:

> insmod test.ko

输出:

===Test Module Start!===

卸载:

rmmod test.ko

输出:

===Test Module Has Been Closed!===

三、错误处理

3.1 Kernel configuration is invalid

make -C /usr/src/kernels/2.6.32-754.6.3.el6.x86_64/ M=/home/maqian/code/hello_netfilter modules
make[1]: Entering directory `/usr/src/linux-2.6.32'

  ERROR: Kernel configuration is invalid.
         include/linux/autoconf.h or include/config/auto.conf are missing.
         Run 'make oldconfig && make prepare' on kernel src to fix it.


  WARNING: Symbol version dump /usr/src/linux-2.6.32/Module.symvers
           is missing; modules will have no dependencies and modversions.

  Building modules, stage 2.
/usr/src/linux-2.6.32/scripts/Makefile.modpost:42: include/config/auto.conf: No such file or directory
make[2]: *** No rule to make target `include/config/auto.conf'.  Stop.
make[1]: *** [modules] Error 2
make[1]: Leaving directory `/usr/src/linux-2.6.32'
make: *** [modules] Error 2

进入到内核代码目录,执行sudo make oldconfig && sudo make prepare,一路回车确认。

3.2 Symbol version dump xxx is missing

make -C /usr/src/kernels/2.6.32-754.6.3.el6.x86_64/ M=/home/maqian/code/hello_netfilter modules
make[1]: Entering directory `/usr/src/linux-2.6.32'

  WARNING: Symbol version dump /usr/src/linux-2.6.32/Module.symvers
           is missing; modules will have no dependencies and modversions.

  Building modules, stage 2.
  MODPOST 0 modules
/bin/sh: scripts/mod/modpost: No such file or directory
make[2]: *** [__modpost] Error 127
make[1]: *** [modules] Error 2
make[1]: Leaving directory `/usr/src/linux-2.6.32'
make: *** [modules] Error 2

还是在刚刚的目录执行:sudo make scripts

当一个模块编译完成之后,使用insmod加载,使用rmmod加载,lsmod可以查看已经挂载的模块,modinfo查看一个模块的详细信息。

3.3 insmod: error inserting 'test.ko': -1 Invalid module format

插入驱动报错:

insmod: error inserting 'test.ko': -1 Invalid module format

dmesg错误:

test: no symbol version for module_layout

检查驱动内核版本是否一致,最开始出现这个问题是因为内核代码是自己手动下载的,后面改成yum下载就好了。

一、基本用法

Makefile中执行shell命令需要在命令前面加上@,例如打印一个变量的值

KERNEL = $(shell uname -r)
INC = /usr/src/kernels/$(KERNEL)/

print:
    @echo $(KERNEL)
    @echo $(INC)

执行make print会有得到理想的输出:

2.6.32-754.6.3.el6.x86_64
/usr/src/kernels/2.6.32-754.6.3.el6.x86_64/

如果不加@符号最后的结果是:

echo 2.6.32-754.6.3.el6.x86_64
2.6.32-754.6.3.el6.x86_64
echo /usr/src/kernels/2.6.32-754.6.3.el6.x86_64/
/usr/src/kernels/2.6.32-754.6.3.el6.x86_64/

可以看到多了几行,分别把每个echo命令也打印出来了。

二、注意事项

2.1 多行命令

当shell脚本中存在多行命令时,换行时要加上\,例如:

@./configure --prefix=/usr/lib \
    aaa=1 bbb=2

2.2 把shell命令的结果赋值给Makefile变量

把shell命令的值赋值需要加上shell关键字,如:

PWD = @(shell pwd)

一、拓扑图

学习linux的网络框架netfilter,想用centos作为路由器,在下面接PC产生流量测试。

默认情况下linux是没有开启数据包转发功能的,需要手动配置,linux使用centos6.9,网络拓扑图如下:

路由器的eth0口接外网,IP地址192.168.123.102,内网口eth0地址10.0.0.x/24,希望PC通过路由nat上网。

二、转发配置

开启路由转发首先检查好防火墙配置,清空原有的nat规则:

iptable -F

然后添加nat规则,一共有两种方式:

第一种:iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -j SNAT --to 192.168.123.102

第二种:iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

其中的部分参数含义为:

  • -t: 类型为nat
  • -A: 添加新规则到规则链的末尾
  • POSTROUTING: 在包就要离开防火墙之前改变其源地址
  • -s: 源地址段,这里设置我的内网地址网段10.0.0.0/24
  • -j SNAT: 满足snat条件的时候跳转
  • --to: 跳转时设置的Ip地址
  • -o: 跳转时的出口设备为eth0

具体可参考防火墙规则,这里设置好后,开启设备的数据包转发:

echo 1 > /proc/sys/net/ipv4/ip_forward

然后快乐的

转发永久生效

上面的echo开启数据包转发只是临时生效,下次重启后就失效了,如果需要永久生效,得修改/etc/sysctl.conf文件,把net.ipv4.ip_forwar设置为1。

> sed -i 's#net.ipv4.ip_forward = 0#net.ipv4.ip_forward = 1#g' /etc/sysctl.conf
> grep net.ipv4.ip_forwar /etc/sysctl.conf # 确认是否修改成功了
net.ipv4.ip_forward = 1 # 已经修改好了
> sysctl -p # 生效配置
net.ipv4.ip_forward = 1 # 这里也可以看到值被修改成了1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
net.ipv4.tcp_syncookies = 1
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296

三、生效性测试

检测是否生效,给PC配置好IP,然后ping百度,同时设备上也开启抓包:

maqian@d2.maqian.co:~$ tcpdump -i eth1 -nnn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes
16:35:42.880920 IP 10.0.0.2.63573 > 223.5.5.5.53: 48582+ A? baidu.com. (27)
16:35:42.924100 IP 223.5.5.5.53 > 10.0.0.2.63573: 48582 2/5/5 A 220.181.57.216, A 123.125.115.110 (229)
16:35:42.938192 IP 10.0.0.2 > 220.181.57.216: ICMP echo request, id 1, seq 11, length 40
16:35:43.021923 IP 220.181.57.216 > 10.0.0.2: ICMP echo reply, id 1, seq 11, length 40
16:35:43.947976 IP 10.0.0.2 > 220.181.57.216: ICMP echo request, id 1, seq 12, length 40
16:35:44.028291 IP 220.181.57.216 > 10.0.0.2: ICMP echo reply, id 1, seq 12, length 40
16:35:44.963608 IP 10.0.0.2 > 220.181.57.216: ICMP echo request, id 1, seq 13, length 40
16:35:45.043968 IP 220.181.57.216 > 10.0.0.2: ICMP echo reply, id 1, seq 13, length 40
16:35:45.979290 IP 10.0.0.2 > 220.181.57.216: ICMP echo request, id 1, seq 14, length 40
16:35:46.058447 IP 220.181.57.216 > 10.0.0.2: ICMP echo reply, id 1, seq 14, length 40
16:35:47.923349 ARP, Request who-has 10.0.0.2 tell 10.0.0.1, length 28
16:35:47.923695 ARP, Reply 10.0.0.2 is-at 00:0c:29:60:22:cc, length 46

区别:

  1. 指针是一个变量类型,引用只是一个变量别名。
  2. 指针可以不用初始化,引用必须初始化。
  3. 指针可以指向空地址,引用不能指向空。
  4. 指针初始化后可以修改,引用不能修改。

其他:

  • 引用本质上也是一个指针,内部实现是一个常量指针。
  • C++中一般建议使用引用,不要使用指针。函数传值建议使用const引用。

 一、条件变量

条件变量是多线程的一种同步方式,它允许多个线程以无竞争的方式等待特定事件发生。无竞争的意思是,当条件满足时,条件满足这个讯号会发送给所有的监听者线程,但多个线程中只有一个能获取到特定事件。条件变量需要配合互斥量一起使用。

相关数据结构和函数:

#include <pthread.h>

// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
       const pthread_condattr_t *restrict attr);
// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

条件变量的数据结构为pthread_cond_t,和其他同步的数据结构一样,也提供了两种方式来初始化。一种是通过赋值的方式,一种是通过函数的方式。在使用pthread_cond_init对初始化条件变量的时候,attr参数一般为NULL。

对条件变量加锁的方式:

#include <pthread.h>

// 等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);
// 带有超时条件的等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);

在对条件变量加锁的时候,需要传入一把已经处于加锁状态的互斥量,此时函数会把当前线程放到条件等待的列表上并对互斥量解锁。此时线程进入条件等待状态,当有信号到达时便会触发。

通知条件满足的函数:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal函数会唤醒条件等待队列上的至少一个线程,pthread_cond_broadcast会唤醒条件队列上的所有线程。

条件队列一般用于消息队列,用于统一、协调多个生产者和消费者之间的竞争关系。

二、使用示例

以下使用了三个线程作为消费者,分别从全局队列上获取消息消费:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include "log.h"

#define THREAD_COUNT 3

// 消息结构
struct msg_st {
    unsigned int msg_id;
    struct msg_st *next;
};

static struct msg_st *msg_queue = NULL;
static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;

// 消费线程函数
void *handle_msg(void *arg) {
    struct msg_st *msg;
    while (1) {
        pthread_mutex_lock(&g_mutex);
        while (msg_queue == NULL) {
            pthread_cond_wait(&g_cond, &g_mutex);
        }

        // 提取消息
        msg = msg_queue;
        msg_queue = msg_queue->next;
        pthread_mutex_unlock(&g_mutex);

        // 退出
        if (msg->msg_id == (unsigned int) -1) {
            info("Thread 0x%x exit!", (unsigned int) pthread_self());
            break;
        }

        info("Thread 0x%x: msg_id = %u", (unsigned int) pthread_self(), msg->msg_id);
    }
    return NULL;
}

// 生产消息函数
void create_msg(struct msg_st *msg) {
    pthread_mutex_lock(&g_mutex);
    msg->next = msg_queue;
    msg_queue = msg;
    pthread_mutex_unlock(&g_mutex);
    pthread_cond_signal(&g_cond);
}

int main() {
    int i;
    struct msg_st msg[10];
    pthread_t pid[THREAD_COUNT];

    debug("start create threads");
    // 创建3个线程作为消费者
    for (i = 0; i < 3; i++) {
        pthread_create(&pid[i], NULL, handle_msg, NULL);
    }

    debug("start create msgs");
    // 生产10个消息
    for (i = 0; i < 10; i++) {
        msg[i].msg_id = i + 1;
        msg[i].next = NULL;
        create_msg(&msg[i]);
    }

    // 休眠1秒,确保所有消息都被消费者消费完成
    sleep(1);

    // 退出所有线程
    debug("start create exit msgs");
    for (i = 0; i < THREAD_COUNT; i++) {
        msg[i].msg_id = (unsigned int) -1;
        msg[i].next = NULL;
        create_msg(&msg[i]);
    }

    // 回收所有线程
    debug("start join threads");
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(pid[i], NULL);
    }

    debug("program end");

    return 0;
}

运行结果,10个消息分别被3个线程打印出来了:

三、其他

3.1 “惊群”效应

条件变量是否存在惊群效应呢?

不会,线程执行wait操作的时候会被放到一个条件等待队列里面去。当条件满足的时候,系统会自动选择队列前面的线程来消费队列。

3.2 为什么wait前要加锁,这样不会死锁吗?

不会,执行wait操作时需要的是一把已经加锁的互斥量,这个锁在wait函数中会解开。

这样做的目的:如果在wait前没有加锁,生产者线程产生了一个消息并发送信号,再执行wait后信号就丢失了。

3.3 生产者的信号为什么放在unlock之后?

wait收到信号之后要对互斥量加锁,此时锁还被生产者持有,消费者依旧还要等待锁的释放,才能持有锁。对消费者而言,最理想的情况就是生产者产生消息后,我立马就能读取消息,此时的互斥量是用来和其他消费者线程同步的,而不是和生产者线程同步。

一、Let's Encrypt

Let's Encrypt SSL证书是一个免费的公益项目,由Mozilla、Cisco、Akamai、IdenTrust、EFF等组织人员发起,主要的目是为了推进网站从HTTP向HTTPS过度的进程,目前已经有越来越多的商家加入和赞助支持。

使用Let's Encrypt生成域名证书的前置条件:

  1. 拥有域名,能自主配置DNS记录。或者提供web服务器验证,需要在网站目录下放一个文件,推荐第一种方式。
  2. 获取证书的环境要能访问到DNS服务器,因为中途需要校验DNS解析。
  3. 拥有主机的超级权限,中途需要更新和安装组件。

二、申请流程

第一步:拉代码,代码开源于Github

> git clone https://github.com/letsencrypt/letsencrypt
Cloning into 'letsencrypt'...
remote: Enumerating objects: 29, done.
remote: Counting objects: 100% (29/29), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 61424 (delta 7), reused 4 (delta 2), pack-reused 61395
Receiving objects: 100% (61424/61424), 20.12 MiB | 5.01 MiB/s, done.
Resolving deltas: 100% (44625/44625), done.

完成后执行命令生成证书:

> ./certbot-auto certonly -d *.maqian.art --manual \
>     --preferred-challenges dns \
>     --server https://acme-v02.api.letsencrypt.org/directory

解释一下各个参数的含义:

  • certonly: 表示当前为安装模式
  • --manual: 表示手动安装插件,不要自动安装了
  • --preferred-challenges dns: 校验方式为dns验证
  • -d *.maqian.art: 要生成的域名列表,可以为多个,如果是多个分别以-d加上即可
  • --server: Let's Encrypt ACME v2版本使用的服务器

接下来的步骤一直接受即可,直到出现添加DNS记录为止:

Requesting to rerun ./certbot-auto with root privileges...
[sudo] password for ma: 
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): maqian@dyxmq.cn

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for sinfor.maqian.io

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.maqian.art with the following value:

zw1MeEkmGZOqSqiySp9Ke8S5a9BXC3O4tYzlbjwU-CU

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

此时需要在DNS服务商处添加dns解析,记录类型为TXT,记录为_acme-challenge,值为zw1MeEkmGZOqSqiySp9Ke8S5a9BXC3O4tYzlbjwU-CU。当DNS记录设置好后,新开一个终端查询解析是否生效:

> nslookup -type=txt _acme-challenge.maqian.art
Server:        100.100.2.136
Address:    100.100.2.136#53

Non-authoritative answer:
_acme-challenge.maqian.art    text = "zw1MeEkmGZOqSqiySp9Ke8S5a9BXC3O4tYzlbjwU-CU"

Authoritative answers can be found from:

当查询到的记录和给定的都一致之后按下任意键执行下一步,如果DNS验证成功就会出现以下信息:

Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/maqian.art-0001/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/maqian.art-0001/privkey.pem
   Your cert will expire on 2019-03-22. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot-auto
   again. To non-interactively renew *all* of your certificates, run
   "certbot-auto renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

此时就表示证书已经申请完成了,存放的路径为:/etc/letsencrypt/live/maqian.art-0001。

> sudo ls /etc/letsencrypt/live/maqian.art-0001/ -l
total 4
lrwxrwxrwx 1 root root  39 Dec 22 23:46 cert.pem -> ../../archive/maqian.art-0001/cert1.pem
lrwxrwxrwx 1 root root  40 Dec 22 23:46 chain.pem -> ../../archive/maqian.art-0001/chain1.pem
lrwxrwxrwx 1 root root  44 Dec 22 23:46 fullchain.pem -> ../../archive/maqian.art-0001/fullchain1.pem
lrwxrwxrwx 1 root root  42 Dec 22 23:46 privkey.pem -> ../../archive/maqian.art-0001/privkey1.pem
-rw-r--r-- 1 root root 692 Dec 22 23:46 README

三、安装到nginx

上面一共生成了四个文件,各自的用途为:

  • cert.pem: Apache服务器端证书
  • chain.pem: Apache根证书和中继证书
  • fullchain.pem: Nginx所需要ssl_certificate文件
  • privkey.pem: 安全证书KEY文件

部署到nginx只需要添加一下指令即可:

ssl on;
ssl_certificate /etc/letsencrypt/live/maqian.art-0001/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/maqian.art-0001/privkey.pem;

打开网站,点开左上角地址栏的https,查看证书: