2020年2月

一、关于SQL注入

sql注入是目前web应用中一种常见的攻击方式,通过恶意构造参数生成不可预期的sql语句,来完成不可告人的秘密。危害极大!它的影响主要有以下两点:

第一:拖库,拖库的意思是直接把整个数据表甚至库中的数据都拖出来了。当今的互联网环境中,数据毫无疑问在任何公司都是最宝贵的财富,一旦数据泄露,轻者造成经济损失,重者可能造成法律责任。

第二:删库,拖库的危害可能只是和他人共享了劳动成果,而删库就不同了,数据被共享了不说,还把数据都删了。这就是典型的——走别人的路,让别人无路可走!

近期闹得沸沸扬扬的“微盟删库”事件,因为运维人员把数据库删了,导致业务接近一周都没有恢复,股价直接下跌10+亿。可见“删库”的危害实在太大!

- 阅读剩余部分 -

使用mysql连接远程服务器时报错,在百度和google查找都没有找到能解决问题的办法:

ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 0

分析应该是以下两个原因导致的:

  1. 服务器有防火墙,禁止3306端口的访问。
  2. 用户没有授权远程访问。

第二个错误首先被排除掉了,根据多年的经验来看,如果是没有权限报错应该是Access Deny或者Permission相关的错误,但是这个错误从没见过。

因此排查的重心就放在了防火墙上了,首先在服务端查看防火墙,防火墙是关闭的状态,并且3306端口允许所有主机访问:

说明不是第一种场景导致的。那么问题来了,这到底是个什么奇葩错误?没办法,只能上终极大招了——抓包。

使用tcpdump抓包:

tcpdump -i eth0 host 192.168.123.17 and tcp port 3306 -nnv -c 100 -w 3306.pcap

然后放到本地用wireshark打开,一个明显的错误就映在眼前了:

1130,没有权限访问,说明还是用户没有权限访问服务器导致:

气到吐血!不知道为什么没有权限客户端是这种鬼错误,直接打印服务端返回来的错误不就行了吗?wtf!

这是一台内网的虚拟机设备,root用户没有开外网访问权限,很久没有使用了不记得了。

解决办法

给root权限加上外网访问权限:

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456';
FLUSH PRIVILEGES;

一、I/O模型分类

unix环境下有5中IO模型:

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O多路复用
  • 信号驱动I/O
  • 异步I/O(POSIX中的aio_系列函数)

常用的是前三种方式,特别是多路I/O复用是目前使用最广泛的I/O模型。它不仅包含了阻塞和非阻塞,同时也包含了异步调用。非阻塞+异步是效率最高的I/O方式。

二、阻塞式I/O

阻塞I/O的意思是:调用读写函数时,系统会卡在当前函数,直到有数据可读或者可写才返回。

工作流程图:

三、非阻塞式I/O

非阻塞I/O的意思是:调用函数时如果没有数据可读,立马返回。然后开始轮询,直到有数据返回为止。

工作流程图:

四、多路I/O复用

多路I/O复用:通过多路IO复用模型(select/poll/epoll)同时监听多个套接字,等待某个套接字有数据到达时再执行系统调用。

工作流程图:

点击左上角Iterm2任务栏,依次选择Preferences - Profile

点击左下角的+新增一个配置项,在右边的command处输入ssh登录的命令:

ssh root@x.x.x.x -p xxxxx

然后把tab页面切换到Advanced,点击Edit进入触发器编辑页面:

新弹框中新增一个触发器,触发器的作用是匹配终端输出的字符串然后执行相应动作。触发字符串是password:,Action选择Send Text,Parameters填入登录密码,密码最后以\n结束表示输完密码后换行:

配置好后退出,在任务栏的Profile中选择创建好的配置就可以自动登录到设备了:

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/invert-binary-tree

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

一、题目描述

翻转一棵二叉树。

示例:

输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

二、题解

简单题,有两种思路:递归和队列。

2.1 递归

每遍历到一个节点,调整左右子树的值。然后分别递归遍历左右子树。

TreeNode *invertTree(TreeNode *root) {
    TreeNode *p;

    if (root == nullptr) {
        return nullptr;
    }

    // 调整左右子树
    p = root->left;
    root->left = root->right;
    root->right = p;

    // 遍历左右子树
    invertTree(root->left);
    invertTree(root->right);

    return root;
}

2.2 使用队列

把根节点放到队列中,然后依次访问队列中的节点,把每个节点的左右子树位置对换,然后把左右子节点放到栈中继续遍历。

TreeNode *invertTree(TreeNode *root) {
    queue<TreeNode *> q;
    TreeNode *node, *tmp;

    if (root == nullptr) {
        return nullptr;
    }

    // 根节点入队
    q.push(root);
    while (!q.empty()) {
        // 取队首元素
        node = q.front();
        q.pop();

        // 调换左右子树
        tmp = node->left;
        node->left = node->right;
        node->right = tmp;

        // 左子树入队
        if (node->left) {
            q.push(node->left);
        }
        // 右子树入队
        if (node->right) {
            q.push(node->right);
        }
    }

    return root;
}

三、备注

这个问题是受到Max Howell 原问题启发的 :

谷歌:我们90%的工程师使用您编写的软件(Homebrew),但是您却无法在面试时在白板上写出翻转二叉树这道题,这太糟糕了。

其中的一个评论给也很有意思:

如果我是你,我会把白板从下到上翻转。然后告诉他:那,这就是我翻转的二叉树。

epoll中的触发模式有两种,边缘触发和水平触发,默认情况下使用的是水平触发。

边缘触发(ET)的意思是当电平出现变化的时候才触发事件,如果设置了边缘触发,执行epoll_wait时,内核检测到数据到达后立马返回到应用层。但是这仅仅只返回这一次,如果缓冲区中的数据没有读取完,再次执行epoll_wait时不会继续触发,需要下一次来数据了才能触发。也就是说,一次数据不会重复发送到应用层,不管你是否读完了。

而水平触发(LT)的意思是只要存在高电平就一直触发事件,执行epoll_wait时,只要检测到有数据就返回。如果缓冲区中存在没有读完的数据,下一次执行epoll_wait还会继续触发事件,无需等到下一次数据来。

两者的触发时间点:

相比之下,ET的效率高于LT模式,因为产生的事件数更少,可以减少内核往应用层空间复制数据的次数。在进行高性能网络编程的时候,往往都是选择非阻塞IO+ET触发模式,这种模式下可以做到想读数据的时候就读,不想读就不读。同时,读不到也不阻塞,大大增加了程序的灵活性。而不是说不管是否想读数据,都强制要求读。

项目中使用ET模式时遇到了一个问题:我们开发了一个socks5代理程序,可以用来代理上网。正常的程序代理都没有问题,就是网页中的实时视频使用代理会出现延时,延时能达到5-10秒。最后查了很久之后发现是ET触发模式导致的, 发送数据的时候内核不会立马发出去,改成LT模式之后就好了。很神奇!

一、猴子拿苹果问题

逛脉脉时,看到一网友遇到的面试题:有9个苹果,2只猴子。一个猴子每次拿2个苹果,一个猴子每次拿3个苹果。如果剩余的苹果数量不够猴子拿的数量,则停止拿苹果。请用多线程的方式模拟上面的描述。

看到问题的第一眼,觉得很有趣,脑海中第一个想到的就是通过信号量来实现,因为信号量是最适合做线程和进程状态同步了,而问题的考点就是线程同步。

恰好这两天刚好有在看信号量,所以很容易就想到通过信号量来实现这个问题。

其实原题说的是通过java多线程实现,但是我不会java,只能用c来写了。

为什么说考查的问题点是线程同步?因为题目要求是通过多线程来实现,如果不对线程状态制约,9个苹果,很容易在一瞬间就被一个猴子拿完,或许第二个线程还没开始苹果就已经拿完了。这明显不是面试官想看到的。所以不难想到考点就是如何协调两个猴子(线程)有序的拿桃子,有序的意思就是:你拿一次,我拿一次,要有次序的进行,直到把桃子拿完。如何控制线程之间你执行一次,我执行一次,肯定就要利用同步了。

二、匿名信号

匿名信号比较适用于线程间同步,因为它没有名字,所以多个进程间无法通过一个载体来共享它。只有在线程中,通过变量或者参数传递的方式来共享,所以适用于线程。

有名信号的用法参考:进程间通信之信号量

匿名信号的原理也和有名信号一样,维持一个计数器,然后通过等待计数器的状态来继续往下执行。相关的函数:

#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 发送信号量
int sem_post(sem_t *sem);
// 等待信号量
int sem_wait(sem_t *sem);
// 关闭信号量
int sem_close(sem_t *sem);

四个函数中,下面三个和有名信号量中的一样,唯一有区别的是第一个初始化信号量函数。匿名信号量无需传入信号量的名字,直接传入信号量对象就可以初始化了。后面的两个参数也都是一样,一个代表共享属性,一个代表默认值。

三、实现猴子拿苹果问题

两个猴子,肯定是需要两个线程,线程中辨别是哪个猴子肯定要传入猴子的参数,因此先实现一个猴子信息的对象,传入线程参数:

// 每个猴子的信息
struct monkey_info {
    int id; // 猴子的ID
    int cnt; // 每次拿苹果的数量
    sem_t *my; // 自己的信号量
    sem_t *other; // 另外一个猴子的信号量
};

为什么需要两个信号量呢,逻辑是:我去拿猴子,拿完通知你拿,你去拿猴子,我等在这里,等你拿完通知我拿。一个是等待自己的信号量,一个是通知另外猴子的信号量,所以需要两个。

线程函数实现:

// 苹果总量
static int s_apple_cnt = 9;

void *get_apple(void *ptr) {
    struct monkey_info *monkey = ptr;
    if (monkey == NULL)
        return NULL;

    while (1) {
        // 等待信号量
        sem_wait(monkey->my);
        if (s_apple_cnt < monkey->cnt) { // 当前的苹果数量不足以给到当前猴子
            printf("monkey %d: apple has't enough! total: %d, need: %d\n", monkey->id, s_apple_cnt, monkey->cnt);
            // 退出前,通知另外猴子继续拿
            sem_post(monkey->other);
            break;
        } else {
            // 拿苹果,减库存
            s_apple_cnt -= monkey->cnt;
            printf("monkey %d: get %d apple! remaining apples: %d\n", monkey->id, monkey->cnt, s_apple_cnt);
        }
        // 通知另外猴子
        sem_post(monkey->other);
    }
    return NULL;
}

主函数实现:

int main() {
    sem_t sem1, sem2;
    pthread_t tid1, tid2;
    // 初始化两个猴子的状态
    struct monkey_info monkey1 = {1, 2, &sem1, &sem2};
    struct monkey_info monkey2 = {2, 3, &sem2, &sem1};

    // 初始化信号
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);

    // 创建线程
    pthread_create(&tid1, NULL, get_apple, (void *)&monkey1);
    pthread_create(&tid2, NULL, get_apple, (void *)&monkey2);

    // 等待线程退出
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    // 退出信号量
    sem_close(&sem1);
    sem_close(&sem2);

    return 0;
}

执行结果:

过程描述:

  1. 第一个猴子拿2个苹果,剩余7个苹果。
  2. 第二个猴子拿3个苹果,剩余4个苹果。
  3. 第一个猴子拿2个苹果,剩余2个苹果。
  4. 第二个猴子想要拿3个苹果,但是苹果不够了,退出。
  5. 第一个猴子拿两个苹果,苹果被拿完。
  6. 第一个猴子想要拿2个苹果,但是苹果不够了,退出。

共享内存是所有IPC通信中效率最高的,它通过把文件映射到用户进程空间,然后直接通过地址访问来实现多进程通信。相对于其他IPC通信方式而言,少去了把数据从用户空间复制到内核空间,再从内核空间复制到用户空间的过程,因此效率相当高。

用图形来表示就是:

操作共享内存的函数:

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t len);

第一个是映射共享内存的函数,参数说明:

  1. addr: 需要映射的共享内存起始地址,一般来说传入NULL。
  2. len: 映射出来的共享内存块大小。
  3. prot: 内存权限,长用的权限是PROT_READ | PROT_WRITE,表示可读可写。
  4. flags: 共享内存区域的属性,可以设置为MAP_SHARED或者MAP_PRIVATE,如果是前者,当前进程对共享内存区域的修改对其他进程可见,否则就不可见。
  5. fd: 共享内存文件的文件描述符。
  6. offset: 文件的偏移,从文件的offset处开始共享内存。

使用示例

以下代码展示了一个共享内存的操作示例,通过共享内存在父子进程间共享一个int类型的变量,然后父进程修改值,子进程读:

代码中忽略掉了一些不相关的错误处理。
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>

int main() {
    int fd, n = 12345;
    int *ptr;
    sem_t *sem;
    pid_t pid;

    // 创建信号
    sem = sem_open("my_sem", O_RDWR | O_CREAT, 0755, 0);
    sem_unlink("my_sem");

    // 往文件写一个数据
    fd = open("mmapfile", O_RDWR | O_CREAT, 0755);
    write(fd, (void *)&n, sizeof(int));

    // 创建共享内存
    ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    pid = fork();
    if (pid == 0) {
        // 子进程,等待父进程修改完共享内存后打印出来
        sem_wait(sem);
        printf("child:\t%d\n", *ptr);
    } else if (pid > 0) {
        // 父进程,把共享内存加一
        printf("parent:\t%d\n", *ptr);
        (*ptr)++;
        sem_post(sem);
    } else {
        perror("fork error");
    }
    
    // 关闭信号量
    sem_close(sem);
    // 取消映射共享内存
    munmap(ptr, sizeof(int));
    // 删掉共享内存文件
    unlink("mmapfile");

    return 0;
}

代码中信号量的作用是确保子进程在父进程执行完成后才执行,父进程先把共享内存的数据加1,然后子进程读取共享内存的数据应该要变成12346。

执行结果符合预期:

一、信号量

信号量有两种,一种的有名信号,一种是无名信号。有名信号一般用于进程间同步,无名信号一般用于于线程间同步。创建或打开一个信号的函数:

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...);

name参数致命信号量的名字,由于信号量内部保存在系统内核中,多个进程间可以直接通过指定信号量的名字来相互通信。oflag表示信号的属性:可读、可写、可执行或者其他,如果指定了O_CREAT属性,则会创建信号量,否则表示打开已有信号量。后面的可选参数有两个:

  1. mode: 信号量的权限,和文件属性一样,0755或者其他。
  2. value: 默认值,信号量内部维持了一个计数,value就是设置计数的默认值。

操作信号量的方法:

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

wait表示等待一个信号量,当信号量中的计数不为0时,会把计数减1,然后继续执行后面的逻辑,如果计数为0,wait操作就会阻塞,直到计数不为0。trywait是wait的非阻塞版本,它会尝试wait,如果计数为0也直接返回。post操作会把计数加1,它和wait相对。只有通过它把计数加1后,阻塞的信号量才最继续往下执行。

关闭信号量:

int sem_close(sem_t *sem);
int sem_unlink(const char *name);

close用于关闭信号量,这个操作会导致信号量的引用计数减1。当引用计数到达0后,执行过unlink的信号量会自动删除。

信号量的主要应用场景是同步,适合同步多个进程的状态同事到达某一个点。

二、示例代码

示例代码中创建了两个进程,每个进程都对一个变量执行三次自加操作。信号量的作用就是保证两个进程对变量自加操作的进度相同(意思是你自加了1次,我也自加1次,不存在你自加2次我才自加1次的情况)。

#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>

#define SEMAPHORE_CHILD_NAME "sem_child"
#define SEMAPHORE_PARENT_NAME "sem_parent"

int main() {
    int i, count;
    sem_t *sem_child, *sem_parent;
    pid_t pid;

    // 创建两个信号量,用于父子进程间相互制约
    sem_child = sem_open(SEMAPHORE_CHILD_NAME, O_RDWR | O_CREAT, 0755, 1);
    sem_parent = sem_open(SEMAPHORE_PARENT_NAME, O_RDWR | O_CREAT, 0755, 1);
    if (sem_child == NULL || sem_parent == NULL) {
        perror("sem_open error");
        return -1;
    }
    // 信号量使用完成后删除
    sem_unlink(SEMAPHORE_CHILD_NAME);
    sem_unlink(SEMAPHORE_PARENT_NAME);

    pid = fork();

    count = 0;
    if (pid == 0) {
        for (i = 0; i < 3; i++) {
            // 等待父进程信号量
            sem_wait(sem_parent);
            count++;
            // 设置子进程信号量
            sem_post(sem_child);
            printf("pid[%u]: count = %d\n", getpid(), count);
        }
    } else if (pid > 0) {
        for (i = 0; i < 3; i++) {
            // 等待子进程信号量
            sem_wait(sem_child);
            count++;
            // 发送父进程信号量
            sem_post(sem_parent);
            printf("pid[%u]: count = %d\n", getpid(), count);
        }
    } else {
        perror("fork error");
    }

    // 关闭信号量
    sem_close(sem_parent);
    sem_close(sem_child);

    return 0;
}

执行结果:

可以看到,两个进程间的字节都是交替进行的。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/word-frequency

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

一、题目描述

写一个 bash 脚本以统计一个文本文件 words.txt 中每个单词出现的频率。

为了简单起见,你可以假设:

  • words.txt只包括小写字母和 ' ' 。
  • 每个单词只由小写字母组成。
  • 单词间由一个或多个空格字符分隔。

示例:

假设 words.txt 内容如下:

the day is sunny the the
the sunny is is

你的脚本应当输出(以词频降序排列):

the 4
is 3
sunny 2
day 1

说明:

  • 不要担心词频相同的单词的排序问题,每个单词出现的频率都是唯一的。
  • 你可以使用一行 Unix pipes 实现吗?

二、题解

2.1 使用awk

通过NF变量遍历所有字段,存到一个哈希表(数组)中,然后打印出所有的key-value组合,最后通过sort排序。

awk '{for (i = 1; i <= NF; i++) {m[$i]++;}} END {for (i in m) {print i, m[i]}}' words.txt | sort -nr -k 2

2.2 使用xargs

通过xargs的-n参数打印出所有的字段,然后使用uniqsort对字段排序:

cat file.txt | xargs -n 1 | sort | uniq -c | sort -nr -k 2 | awk '{print $2" "$1}'
uniq的-c参数是统计词频