2019年5月

一、UDP协议

UDP协议是一种简单的面向数据报的传输层协议,它不提供差错纠正、队列管理、重复消除、流量控制以及拥塞控制等功能。它只提供最小的差错检测功能,要想保证数据被可靠的投递或正确排序,应用程序必须自己实现这些功能。

因为它是无连接协议,所以它比其他传输层协议使用更少的开销。

UDP的首部如下:

UDP首部共包含8个字节,内容很简单,仅仅包含了源目端口号、报文长度以及校验和这四个元素。其中,长度字段表示的是数据长度和首部长度

二、UDP校验和

UDP校验和要用到一个伪首部,用到来自IP数据报中的部分字段,伪首部的结构如下:

伪头部的目的是让UDP层验证数据是否已经到达正确目的地(即,该IP没有接受地址错误的数据报,也没有给UDP一个其他传输协议的数据报),它不会被发送出去,只用作校验功能。

注意

  1. UDP数据报长度可以是奇数个字节,而校验算法只相加16位(2字节),因此如果UDP长度是奇数时,计算校验和会在末尾补零(但不会被发送出去)。
  2. UDP中的校验和是可选的(强烈建议使用)。因此对于使用UDP的应用层协议,在收到数据时应当先校验校验和。

三、UDP/IP分片

UDP数据报文最大可达2^16=65535个字节,除去首部还剩下65527个字节。但是受限于网络中的MTU等设置,过长的UDP数据报文段将会在IP数据报中分片。例如在一个MTU为1500的网络环境中发送一个长度为3000的UDP报文将会以如下形式发送出去:

注意:

  1. 偏移的单位是8个字节,例如第二片的偏移是185,实际上是第185*8=1480个字节。

一、常用端口号

  • 20/21: FTP端口,20端口用来传输数据,21端口用来处理连接
  • 22: ssh端口,安全shell协议
  • 23: telnet端口,telnet服务协议
  • 25/465/587: smtp端口,发邮件协议
  • 53: dns端口,域名解析协议,基于UDP
  • 80: web服务端口,http超文本传输协议
  • 110/995: pop3端口,收邮件协议
  • 115: sftp端口,安全文件传输协议
  • 123: ntp端口,网络时间协议
  • 143/993: IMAP协议端口,收邮件协议
  • 179: bgp端口,边界网络协议
  • 389: ldap端口,轻型目录存储协议
  • 443: https端口,安全超文本传输协议

二、常用网络命令

2.1 ifconfig、ifup和ifdown

启动和关闭网卡:

> ifconfig eth0 up
> ifconfig eth0 down

设置网卡IP地址:

> ifconfig eth0 192.168.10.2
> ifconfig eth0:1 192.168.20.2 # 多IP
> ifconfig eth0 192.168.10.2/24 # ip地址+掩码位数
> ifconfig eth0 192.168.10.2 netmask 255.255.255.0 # ip地址+子网地址
> ifconfig eth0 192.168.10.2/24 broadcast 192.168.10.255 # 指定广播地址

设置IPv6地址:

# 添加IPv6地址
> ifconfig eth0:1 inet6 add abcd::ff10
> ifconfig eth0:2 inet6 add abcd::ff11
# 删除IPv6地址
> ifconfig eth0 inet6 del abcd::ff10/0
> ifconfig eth0 inet6 del abcd::ff11/0

ifupifdown分别用于开启和关闭网卡:

> ifup eth0 # 开启eth0
> ifdown eth0 # 关闭eth0

2.2 route

添加、删除静态路由:

> route [add|del] -net 192.168.1.0 netmask 255.255.255.0 gw 192.168.2.254

设置默认网关:

> route add default gw 192.168.2.254

打印所有的路由:

> route -n # linux
> route print # windows

2.3 dig和nslookup

查询dns命令,dig会打印出解析过程和协议:

> dig www.baidu.com

nslookup打印所有的结果:

> nslookup -t=CNAME www.baidu.com

2.4 traceroute

查询去往一个主机所经过的路由跳数:

> traceroute www.baidu.com

2.5 telnet

一般用于探测服务或端口是否连接正常:

> telnet 129.168.10.1 443

2.6 ethtool

查看和更改网卡的配置

> ethtool -p eth0 # 测试一个网卡,网卡灯会闪烁
> ethtool -s eth0 # 统计网络信息

2.7 netstat

常用参数:

  • -t: 列出TCP套接字
  • -u: 列出UDP套接字
  • -p: 显示PID和进程名字
  • -l: 列出监听状态的socket
  • -n: 使用IP地址而不是解析的域名

查询端口占用情况:

> netstat -apn | grep 443

查询所有处于监听状态的tcp套接字

> netstat -lt

2.8 ping

探测网络是否连通:

> ping www.baidu.com -c 4 # 探测www.baidu.com,只发送四次

windows下默认是发四个包,如果要持续发包使用-t选项:

> ping www.baidu.com -t

2.9 curl和wget

curl命令用来发送get和post请求:

  • -X [POST|DELETE]: http请求类型
  • -H: 添加头部
  • -d: 数据部分
  • -I: 只显示响应头
> curl www.baidu.com
> curl -X POST -H "Content-Type: application/json" \
          -d '{"name": "maqian", "age": 22}' www.baidu.com

wget多用于下载文件:

> wget www.baidu.com/index.html -O baidu.html # -O选项表示写到文件

一、匿名管道

pipe的基本描述和用法

匿名管道是linux中的一种通信方式,为有血缘关系的进程提供数据通信。相关的api函数为:

#include <unistd.h>
int pipe(int pipefd[2]);

pipe函数传入一个长度为2的int数组,执行成功将在内核区域分配一块内存区域,并设置pipefd为管道的文件描述符。

创建子进程后,子进程也会继承这两个描述符,对于同一个文件描述符来说,父端写,子端就可以读,字段写,父端也可以读。

工作流程可以描述为:

但是由于管道内部实现是用的环形队列,因此父子不能同时对一个管道进行读操作或者写操作,队列是单向的,两端写就有问题。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>

#define SEND_BUFF "HelloWorld"

#define CLOSE_FD(fd) \
    do {    \
        if (fd != -1) { \
            close(fd);  \
            fd = -1;    \
        }   \
    } while (0)

int main(){
    int fd[2], ret, n;
    char buf[1025];

    ret = pipe(fd);
    if (ret == -1)
        goto _err;

    memset(buf, 0, 1025);
    pid_t pid = fork();

    if (pid == -1) {
        goto _err;
    } else if (pid == 0) {
        // 子端关闭fd1,打开fd0读
        CLOSE_FD(fd[1]);
        n = read(fd[0], buf, 1024);
        if (-1 == n) goto _err;
        printf("child read %d byte data: %s\n", n, buf);
        CLOSE_FD(fd[0]);
    } else {
        // 父端关闭fd0,打开fd1写
        CLOSE_FD(fd[0]);
        n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
        if (n == -1) goto _err;
        printf("parent send %d bytes data: %s\n", 
                sizeof(SEND_BUFF), SEND_BUFF);
        // 等待子进程退出
        wait(0);
        CLOSE_FD(fd[1]);
    }
    return 0;

_err:
    perror("Error");
    CLOSE_FD(fd[0]);
    CLOSE_FD(fd[1]);
    return -1;
}

1.2 非阻塞pipe

管道还有一个函数pipe2提供了更高级的选项,可以使得管道成为非阻塞读:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

其中,当flags置为O_NONBLOCK时,管道为非阻塞,从非阻塞的管道读取数据时,如果此时没有数据可读,将返回-1,并设置errno为EAGAIN

修改子进程部分代码为:

if (pid == 0){
    CLOSE_FD(fd[1]);
    while (1) {
        n = read(fd[0], buf, 1024);
        if (-1 == n) {
            if (errno == EAGAIN) {
                printf("READ AGAIN\n");
                // 没有数据可读时,休眠一秒,继续读
                sleep(1);
                continue;
            }
            goto _err;
        }
        printf("child read %d byte data: %s\n", n, buf);
        break;
    }
    CLOSE_FD(fd[0]);
}

父进程在写数据前先睡眠5秒:

else {
    CLOSE_FD(fd[0]);
    sleep(5);
    n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
    // ...
}

运行结果:

1.3 管道数据读取的原则

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭,这时有进程指向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
  4. 如果有指向管道读端的文件描述符没关闭,而持有管道写端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再write会阻塞,直到管道中有空位置了才写入数据并返回。

二、有名管道fifo

2.1 fifo的基本用法

fifo管道是linux文件系统中的一种文件类型,创建在磁盘上,任何进程都可以打开这个文件进行读写。

它可以解决无血缘关系进程的通信问题,创建管道的函数为:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

当管道文件被创建之后,就可像文件一样打开它进行读写:

// read.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include <errno.h>
#include<unistd.h>

#define FIFO_FILE "fifo"
#define BUFF_SIZE 1024

int main() {
    int ret, fd = -1;
    char buf[BUFF_SIZE] = { 0 };

    ret = mkfifo(FIFO_FILE, 0755);
    if (ret == -1 && errno != EEXIST)
        goto _err;

    fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1)
        goto _err;

    bzero(buf, BUFF_SIZE);
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1)
        goto _err;

    printf("read: %s\n", buf);
    close(fd);
    fd = -1;
    
    return 0;
_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

写管道程序:

// write.c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

#define FIFO_FILE "fifo"

int main(){
    int ret, fd;

    fd = open(FIFO_FILE, O_WRONLY);
    if (fd == -1)
        goto _err;
        
    unlink(FIFO_FILE);

    ret = write(fd, "HelloWorld", 11);
    if (ret == -1)
        goto _err;

    close(fd);
    fd = -1;

    return 0;

_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

此时运行./read,会创建管道文件,新开一个窗口就可以看到,管道的颜色和普通文件不一样,并且ls的第一个字符是p

默认情况下,读是阻塞的(类似于读文件),直到有数据过来为止,所以./read执行后,会阻塞,直到运行./write之后才会收到数据。

read: HelloWorld

2.2 非阻塞pipe

非阻塞pipe有一个注意事项:当打开的管道没有其他进程也打开的话(即只有当前进程在读,没有其他进程了),直接返回0,不会返回EAGAGIN

// 加上O_NONBLOCK标记
fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK);
if (fd == -1)
    goto _err;

bzero(buf, BUFF_SIZE);
printf("fd = %d\n", fd);
while (1) {
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1) {
        if (errno == EAGAIN) {
            printf("READ AGAIN\n");
            sleep(1);
            continue;
        }
        goto _err;
    } else if (ret == 0) {
        printf("read end\n");
        close(fd);
        fd = -1;
        break;
    } else {
        printf("read: %s\n", buf);
    }
}

编译后如果直接执行,程序会直接退出,因为此刻没有其他进程在写,不会返回EAGAIN

一、线程同步的几种方法

多线程主要有以下几种同步方法:

  1. 互斥量
  2. 读写锁
  3. 屏障
  4. 条件变量
  5. 信号量
  6. 自旋锁

二、几种同步方式的比较

同步方式优缺点和适用范围
互斥量最简单的锁,使用临界区的方式锁住代码块,最基础的锁,适用于多线程互斥
读写锁有更高的并行性,只会锁住写锁,读锁共享,适用于读远大于写的场景
屏障适用于线程等待,直到多个线程到达同一点时再继续运行,主要用于同步
条件变量允许线程以无竞争的方式等待特定条件的发生,适用于生产者和消费者场景
信号量通过PV变量的方式,多用于生产者和消费者场景,和条件变量类似
自旋锁被锁住时线程处于忙等,适用于锁被持有的时间短,不希望线程把时间花在重新调度上

三、信号量和条件变量的区别

  1. 条件变量需要用到互斥锁,信号量不用
  2. 信号量是基于PV操作,需要多个信号量配合使用
  3. 信号量可以用于线程和进程,条件变量只能用于线程