wine99

wine99 查看完整档案

合肥编辑合肥工业大学  |  计算机科学与技术 编辑  |  填写所在公司/组织 wine99.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

wine99 发布了文章 · 2月18日

CS144 Lab Assignments - 手写TCP - LAB4

CS 144: Introduction to Computer Networking, Fall 2020
https://cs144.github.io/

My Repo
https://github.com/wine99/cs1...

任务

本节实现 TCPConnection 类,实例化这个类将作为一个完整的 TCP 连接中的一个 peer(可以充当任意一方,Server 或 Client)。前面两个实验分别实现的 TCPSender 和 TCPReceiver 并不能作为一个独立的 Server 或 Client,这两个类的实例是用作 TCPConnection 实例的内部成员,即下图。

ygfFsI.png

Sender 和 Receiver 的作用

  • 收到报文段时

    • 通知 _receiver:根据报文段的 seqno、SYN、FIN 和 payload,以及当前状态,更新 ackno;收集数据
    • 通知 \_sender:根据报文段的 ackno 以及当前状态,更新 next_seqno;更新 window_size
  • 发送报文段时

    • \_sender 负责填充 payload、seqno、SYN、FIN,注意有可能既没有 payload 也没有 S、F 标识(empty segment),这和 Lab3 实现的 \_sender 的 ack_received() 逻辑不同
    • \_receiver 负责填充 ackno、window size

FSM

结合 Lab2、Lab3 讲义中的 TCPSender 和 TCPReceiver 的状态转换图,tcp_state.cc 中 TCPConnection 的各状态与 sender、receiver 状态的对应关系,以及下面的 TCPConnection 的状态转换图,理解整个 TCP 连接。

yg5iQJ.jpg

Edge case

在实现过程中,需要额外关注收到报文段时 TCPSender 和 TCPConnection 的逻辑的不同之处。这些细节来源于

  1. Lab2 中的 receiver 只关心收到数据和数据有关的标识;Lab3 中 sender 只关心收到的 ackno 和 win,不处理也不知道收到的数据和其他信息,在 \_stream_in() 没有数据时可能不会做任何动作(我的 Lab3 实现是这样的),而在 Lab4 中可能还需要发一个空的 ACK 报文段
  2. 连接建立和释放过程中的各种特殊情况

    1. 发完 SYN 后马上收到 RST
    2. 发完 SYN 后马上收到 FIN
    3. Simultaneous open
    4. Simultaneous shutdown
    5. ...

实验给出的测试套非常完备,覆盖了各种特殊情况,Simultaneous open 和 Simultaneous shutdown 的情况见下图。按照讲义所说,如果你的 Lab2 和 Lab3 实现非常 robust,Lab4 的大部分工作是 wire up 前面两个类的接口,但也有可能你需要修改前两个实验的实现。

下图出处:TCP State Transitions

yg7ih6.jpgyg7A1O.jpg

实现

我的实验四的函数框架参考了 这篇博客,但实现不同。我在网上浏览过的几个实现,均改动了 Lab2、Lab3 的函数签名,让 Lab2、Lab3 的实现变得不太干净。我的最终实现没有入侵 Lab3 和 Lab2 的代码,细节逻辑全部在 TCPConnection 类中完成。

注意如果 tests 文件夹中的测试全部通过但是 txrx.sh 中的测试不通过,并且不通过的原因是结果的哈希值不同,去掉所有的自己添加的打印语句,再进行测试。

实验四刚开始时一度想要放弃,但最终花费的时间居然比实验三要少(实验三零零碎碎花了六天左右,实验四大概花费了集中的两天半时间)。通过全部测试的时候,还感觉有点懵逼,怎么就通过了,我真的把细节都处理完了?第一次意识到,复杂的项目中,完备的测试比“充满自信”的实现代码可靠多了,也不得不感慨课程质量之高以及讲师和助教付出的心血。

y2kzRO.png

代码

添加的成员变量

class TCPConnection {
  private:
    size_t _time_since_last_segment_received{0};
    bool _active{true};

    void send_sender_segments();
    void clean_shutdown();
    void unclean_shutdown();

实现代码

#include "tcp_connection.hh"

#include <iostream>

using namespace std;

size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }

size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }

size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }

size_t TCPConnection::time_since_last_segment_received() const { return _time_since_last_segment_received; }

bool TCPConnection::active() const { return _active; }

void TCPConnection::segment_received(const TCPSegment &seg) {
    if (!_active)
        return;
    _time_since_last_segment_received = 0;
    // State: closed
    if (!_receiver.ackno().has_value() && _sender.next_seqno_absolute() == 0) {
        if (!seg.header().syn)
            return;
        _receiver.segment_received(seg);
        connect();
        return;
    }
    // State: syn sent
    if (_sender.next_seqno_absolute() > 0 && _sender.bytes_in_flight() == _sender.next_seqno_absolute() &&
        !_receiver.ackno().has_value()) {
        if (seg.payload().size())
            return;
        if (!seg.header().ack) {
            if (seg.header().syn) {
                // simultaneous open
                _receiver.segment_received(seg);
                _sender.send_empty_segment();
            }
            return;
        }
        if (seg.header().rst) {
            _receiver.stream_out().set_error();
            _sender.stream_in().set_error();
            _active = false;
            return;
        }
    }
    _receiver.segment_received(seg);
    _sender.ack_received(seg.header().ackno, seg.header().win);
    // Lab3 behavior: fill_window() will directly return without sending any segment.
    // See tcp_sender.cc line 42
    if (_sender.stream_in().buffer_empty() && seg.length_in_sequence_space())
        _sender.send_empty_segment();
    if (seg.header().rst) {
        _sender.send_empty_segment();
        unclean_shutdown();
        return;
    }
    send_sender_segments();
}

size_t TCPConnection::write(const string &data) {
    if (!data.size())
        return 0;
    size_t write_size = _sender.stream_in().write(data);
    _sender.fill_window();
    send_sender_segments();
    return write_size;
}

//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) {
    if (!_active)
        return;
    _time_since_last_segment_received += ms_since_last_tick;
    _sender.tick(ms_since_last_tick);
    if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS)
        unclean_shutdown();
    send_sender_segments();
}

void TCPConnection::end_input_stream() {
    _sender.stream_in().end_input();
    _sender.fill_window();
    send_sender_segments();
}

void TCPConnection::connect() {
    _sender.fill_window();
    send_sender_segments();
}

TCPConnection::~TCPConnection() {
    try {
        if (active()) {
            cerr << "Warning: Unclean shutdown of TCPConnection\n";
            _sender.send_empty_segment();
            unclean_shutdown();
        }
    } catch (const exception &e) {
        std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
}

void TCPConnection::send_sender_segments() {
    TCPSegment seg;
    while (!_sender.segments_out().empty()) {
        seg = _sender.segments_out().front();
        _sender.segments_out().pop();
        if (_receiver.ackno().has_value()) {
            seg.header().ack = true;
            seg.header().ackno = _receiver.ackno().value();
            seg.header().win = _receiver.window_size();
        }
        _segments_out.push(seg);
    }
    clean_shutdown();
}

void TCPConnection::unclean_shutdown() {
    // When this being called, _sender.stream_out() should not be empty.
    _receiver.stream_out().set_error();
    _sender.stream_in().set_error();
    _active = false;
    TCPSegment seg = _sender.segments_out().front();
    _sender.segments_out().pop();
    seg.header().ack = true;
    if (_receiver.ackno().has_value())
        seg.header().ackno = _receiver.ackno().value();
    seg.header().win = _receiver.window_size();
    seg.header().rst = true;
    _segments_out.push(seg);
}

void TCPConnection::clean_shutdown() {
    if (_receiver.stream_out().input_ended()) {
        if (!_sender.stream_in().eof())
            _linger_after_streams_finish = false;
        else if (_sender.bytes_in_flight() == 0) {
            if (!_linger_after_streams_finish || time_since_last_segment_received() >= 10 * _cfg.rt_timeout) {
                _active = false;
            }
        }
    }
}

性能优化

分析

由于没有做过 profiling,性能分析的工作抄了上面提到的博客的作业。

修改 sponge/etc/cflags.cmake 中的编译参数,将-g改为-Og -pg,使生成的程序具有分析程序可用的链接信息。

make -j8
./apps/tcp_benchmark
gprof ./apps/tcp_benchmark > prof.txt

y2nkwQ.png

如讲义中所说,很可能需要改动 ByteStream 或 StreamReassembler。调优方法是利用 buffer.h 中提供的 BufferList。实际上测试代码中就有用到 BufferList,简而言之它是一个 deque\<Buffer\>,而 Buffer 则在整个实现与测试代码中被大量使用,例如 payload() 就是一个 Buffer 实例。

改动

把 ByteStream 类中字节流的容器由 Lab0 最初的 std::list<char> _stream{}; 改为 BufferList _stream{};

byte_stream.cc 改动的函数:

size_t ByteStream::write(const string &data) {
    size_t write_count = data.size();
    if (write_count > _capacity - _buffer_size)
        write_count = _capacity - _buffer_size;
    _stream.append(BufferList(move(string().assign(data.begin(), data.begin() + write_count)))); 
    _buffer_size += write_count;
    _bytes_written += write_count;
    return write_count;
}

//! \param[in] len bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
    const size_t peek_length = len > _buffer_size ? _buffer_size : len;
    string str = _stream.concatenate();
    return string().assign(str.begin(), str.begin() + peek_length);
}

//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
    size_t pop_length = len > _buffer_size ? _buffer_size : len;
    _stream.remove_prefix(pop_length);
    _bytes_read += pop_length;
    _buffer_size -= pop_length;
}

改动后的 benchmark

y2KfoQ.png

webget revisited

直接按照讲义中的步骤,把 Linux 自带的 TCPSocket,换成我们自己的实现。

void get_URL(const string &host, const string &path) {
    CS144TCPSocket sock1{};
    sock1.connect(Address(host, "http"));
    sock1.write("GET " + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
    while (!sock1.eof()) {
        cout << sock1.read();
    }
    sock1.shutdown(SHUT_WR);
    sock1.wait_until_closed();
}

替换后 webget 依然 work(不知道为什么 WSL 替换后连接建立不起来,但在云主机上测试后没有问题),至此,手写 TCP 正式完成。

查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月16日

CS144 Lab Assignments - 手写TCP - LAB3

CS 144: Introduction to Computer Networking, Fall 2020
https://cs144.github.io/

My Repo
https://github.com/wine99/cs1...

总体思路

tick 不需要我们来调用,参数的意义是距离上次 tick 被调用过去的时间,也不需要我们来设定。我们只需要在 tick 中实现,通过参数判断过去了多少时间,需要执行何种操作即可。

注意根据文档,我们要不需要实现选择重传,而是类似回退 N,需要存储已发送并且未被确认的段,进行累计确认,超时时只要重传这些段中最早的那一个即可。

TCPReceiver 调用 unwrap 时的 checkpoint 是上一个接收到的报文段的 absolute_seqno,TCPSender 调用 unwrap 时的 checkpoint 是 _next_seqno

我的实现中计时器开关的处理:

  • 发送新报文段时若计时器未打开,开启
  • ack_received() 中,如果有报文段被正确地确认,重置计时器和 RTO,如果所有报文段均被确认(bytes in flight == 0),关闭计时器
  • tick() 中,若计时器为关闭状态,直接返回,否则累加计时然后处理超时

添加的成员变量

class TCPSender {
  private:
    bool _syn_sent = false;
    bool _fin_sent = false;
    uint64_t _bytes_in_flight = 0;
    uint16_t _receiver_window_size = 0;
    uint16_t _receiver_free_space = 0;
    uint16_t _consecutive_retransmissions = 0;
    unsigned int _rto = 0;
    unsigned int _time_elapsed = 0;
    bool _timer_running = false;
    std::queue<TCPSegment> _segments_outstanding{};

    bool _ack_valid(uint64_t abs_ackno);
    void _send_segment(TCPSegment &seg);
};
  • send_segment(TCPSegment &seg) 只在 fill_window() 中被调用,重传只需要 _segments_out.push(_segments_outstanding.front())
  • _receiver_window_size 保存收到有效(有效的含义见上面 ack_valid())确认报文段时,报文段携带的接收方窗口大小
  • _receiver_free_space 是在 _receiver_window_size 的基础上,再减去已发送的报文段可能占用的空间(_bytes_in_flight

fill_window() 实现

  • 如果 SYN 未发送,发送然后返回
  • 如果 SYN 未被应答,返回
  • 如果 FIN 已经发送,返回
  • 如果 _stream 暂时没有内容但并没有 EOF,返回
  • 如果 _receiver_window_size 不为 0

    1. receiver_free_space 不为 0,尽可能地填充 payload
    2. 如果 _stream 已经 EOF,且 _receiver_free_space 仍不为 0,填上 FIN(fin 也会占用 _receiver_free_space)
    3. 如果 _receiver_free_space 还不为 0,且 _stream 还有内容,回到步骤 1 继续填充
  • 如果 _receiver_window_size 为 0,则需要发送零窗口探测报文

    • 如果 _receiver_free_space 为 0

      • 如果 _stream 已经 EOF,发送仅携带 FIN 的报文
      • 如果 _stream 还有内容,发送仅携带一位数据的报文
    • 之所以还需要判断 _receiver_free_space 为 0,是因为这些报文段在此处应该只发送一次,后续的重传由 tick() 函数控制,而当发送了零窗口报文段后 _receiver_free_space 的值就会从原来的与 _receiver_window_size 相等的 0 变成 -1
void TCPSender::fill_window() {
    if (!_syn_sent) {
        _syn_sent = true;
        TCPSegment seg;
        seg.header().syn = true;
        send_segment(seg);
        return;
    }
    if (!_segments_outstanding.empty() && _segments_outstanding.front().header().syn)
        return;
    if (!_stream.buffer_size() && !_stream.eof())
        return;
    if (_fin_sent)
        return;

    if (_receiver_window_size) {
        while (_receiver_free_space) {
            TCPSegment seg;
            size_t payload_size = min({_stream.buffer_size(),
                                       static_cast<size_t>(_receiver_free_space),
                                       static_cast<size_t>(TCPConfig::MAX_PAYLOAD_SIZE)});
            seg.payload() = _stream.read(payload_size);
            if (_stream.eof() && static_cast<size_t>(_receiver_free_space) > payload_size) {
                seg.header().fin = true;
                _fin_sent = true;
            }
            send_segment(seg);
            if (_stream.buffer_empty())
                break;
        }
    } else if (_receiver_free_space == 0) {
        // The zero-window-detect-segment should only be sent once (retransmition excute by tick function).
        // Before it is sent, _receiver_free_space is zero. Then it will be -1.
        TCPSegment seg;
        if (_stream.eof()) {
            seg.header().fin = true;
            _fin_sent = true;
            send_segment(seg);
        } else if (!_stream.buffer_empty()) {
            seg.payload() = _stream.read(1);
            send_segment(seg);
        }
    }
}

void TCPSender::send_segment(TCPSegment &seg) {
    seg.header().seqno = wrap(_next_seqno, _isn);
    _next_seqno += seg.length_in_sequence_space();
    _bytes_in_flight += seg.length_in_sequence_space();
    if (_syn_sent)
        _receiver_free_space -= seg.length_in_sequence_space();
    _segments_out.push(seg);
    _segments_outstanding.push(seg);
    if (!_timer_running) {
        _timer_running = true;
        _time_elapsed = 0;
    }
}

ack_received() 实现

代码比较直白,注意进行累计确认之后,如果还有未被确认的报文段,_receiver_free_space 的值应为:收到的确认号绝对值 + 窗口大小 - 首个未确认报文的序号绝对值 - 未确认报文段的长度总和。

void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
    uint64_t abs_ackno = unwrap(ackno, _isn, _next_seqno);
    if (!ack_valid(abs_ackno)) {
        // cout << "invalid ackno!\n";
        return;
    }
    _receiver_window_size = window_size;
    _receiver_free_space = window_size;
    while (!_segments_outstanding.empty()) {
        TCPSegment seg = _segments_outstanding.front();
        if (unwrap(seg.header().seqno, _isn, _next_seqno) + seg.length_in_sequence_space() <= abs_ackno) {
            _bytes_in_flight -= seg.length_in_sequence_space();
            _segments_outstanding.pop();
            // Do not do the following operations outside while loop.
            // Because if the ack is not corresponding to any segment in the segment_outstanding,
            // we should not restart the timer.
            _time_elapsed = 0;
            _rto = _initial_retransmission_timeout;
            _consecutive_retransmissions = 0;
        } else {
            break;
        }
    }
    if (!_segments_outstanding.empty()) {
        _receiver_free_space = static_cast<uint16_t>(
            abs_ackno + static_cast<uint64_t>(window_size) -
            unwrap(_segments_outstanding.front().header().seqno, _isn, _next_seqno) - _bytes_in_flight);
    }

    if (!_bytes_in_flight)
        _timer_running = false;
    // Note that test code will call it again.
    fill_window();
}

// See test code send_window.cc line 113 why the commented code is wrong.
bool TCPSender::_ack_valid(uint64_t abs_ackno) {
    return abs_ackno <= _next_seqno &&
           //  abs_ackno >= unwrap(_segments_outstanding.front().header().seqno, _isn, _next_seqno) +
           //          _segments_outstanding.front().length_in_sequence_space();
           abs_ackno >= unwrap(_segments_outstanding.front().header().seqno, _isn, _next_seqno);
}

tick() 实现

注意,窗口大小为 0 时不需要增加 RTO。但是发送 SYN 时,窗口为初始值也为 0,而 SYN 超时是需要增加 RTO 的。

void TCPSender::tick(const size_t ms_since_last_tick) {
    if (!_timer_running)
        return;
    _time_elapsed += ms_since_last_tick;
    if (_time_elapsed >= _rto) {
        _segments_out.push(_segments_outstanding.front());
        if (_receiver_window_size || _segments_outstanding.front().header().syn) {
            ++_consecutive_retransmissions;
            _rto <<= 1;
        }
        _time_elapsed = 0;
    }
}

其他代码

#include <algorithm>

TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn)
    : _isn(fixed_isn.value_or(WrappingInt32{random_device()()}))
    , _initial_retransmission_timeout{retx_timeout}
    , _stream(capacity)
    , _rto{retx_timeout} {}

uint64_t TCPSender::bytes_in_flight() const { return _bytes_in_flight; }

unsigned int TCPSender::consecutive_retransmissions() const { return _consecutive_retransmissions; }

void TCPSender::send_empty_segment() {
    TCPSegment seg;
    seg.header().seqno = wrap(_next_seqno, _isn);
    _segments_out.push(seg);
}
查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月10日

CS144 Lab Assignments - 手写TCP - LAB2

CS 144: Introduction to Computer Networking, Fall 2020
https://cs144.github.io/

My Repo
https://github.com/wine99/cs1...

Translating between 64-bit indexes and 32-bit seqnos

ydf2M4.png

注意 wrapping_integers.hh 中的这三个方法:

inline int32_t operator-(WrappingInt32 a, WrappingInt32 b) { return a.raw_value() - b.raw_value(); }

//! \brief The point `b` steps past `a`.
inline WrappingInt32 operator+(WrappingInt32 a, uint32_t b) { return WrappingInt32{a.raw_value() + b}; }

//! \brief The point `b` steps before `a`.
inline WrappingInt32 operator-(WrappingInt32 a, uint32_t b) { return a + -b; }

下面的两个加减,分别是在 WrappingInt32 上加上或减去一个 uint32_t,得到的结果仍然是一个 WrappingInt32,其意义是分别为把 a 这个 WrappingInt32 向前或向后移动 |b| 个单位距离。

而第一个减法重载,是两个 WrappingInt32 相减,得到的是一个 int32_t,要想理解其意义,首先要弄懂下面的代码:

// 2 ^ 32 = 4294967296

uint32_t a = 1;
uint32_t b = 0;
uint32_t x = a - b;
int32_t  y = a - b;
// x=1 y=1
uint32_t x = b - a;
int32_t  y = b - a;
// x=4294967295 y=-1

uint32_t a = 1;
uint32_t b = static_cast<uint32_t>((1UL << 32) - 1UL);
uint32_t x = a - b;
int32_t  y = a - b;
// x=2 y=2
uint32_t x = b - a;
int32_t  y = b - a;
// x=4294967294 y=-2

上面的运算说明:c(int32_t) = a(uint32_t) - b(uint32_t) 的 c 的绝对值的意义是从 b 走到 a 需要花费的最少步数,如果 c 是正数,则向数轴的正方向走,否则向反方向走。之所以存在最小步数一说,是因为往反方向走穿越 0 会到 2^32 - 1,往正方向走穿越 2^32 - 1 会回到 0。这个步数也一定不会超过 2^31 步。第一个减法也是这样的意义,代表了从 WrappingInt32 b 走到 WrappingInt32 a 最少需要的步数。

理解了这三个运算符重载后,就可以编写 Wrapping_integers.cc 了:

WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) { return isn + static_cast<uint32_t>(n); }

uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
    // STEP ranges from -UINT32_MAX/2 to UINT32_MAX/2
    // in most cases, just adding STEP to CHECKPOINT will get the absolute seq
    // but if after adding, the absolute seq is negative, it should add another (1UL << 32)
    // (this means the checkpoint is near 0 so the new seq should always go bigger
    // eg. test/unwrap.cc line 25)
    int32_t steps = n - wrap(checkpoint, isn);
    int64_t res = checkpoint + steps;
    return res >= 0 ? checkpoint + steps : res + (1UL << 32);
}

首先看把 64 位的 index 转换为 WrappingInt32 的 wrap 函数。根据上面的表格,显然,只需要利用加法重载,把 ISN 往前走 n(=index) 步就可以了。

然后看 unwrap 函数,起作用是把新接收到的报文段的 seqno(WrappingInt32)转换为 64 位的 index。seqno 转 index 的结果显然不唯一,我们想要的是与上一次收到的报文段的 index(checkpoint)最接近的那个转换结果。于是,可以先用刚写好的 wrap 函数把 checkpoint 变为 WrappingInt32,然后利用第一个减法重载,找出这个转换后的 seqno 最少需要走几步可以到新报文的 seqno,然后把这个步数加到 checkpoint 上。注意这里有一个特殊情况(见代码注释),因为我们有可能是往数轴的反方向走的,可能走完之后 res 的值是个负数,这时候需要在加上一个 2^32。

Implementing the TCP receiver

注意测试里面的特殊情况,例如

  • SYN with DATA
  • SYN with DATA with FIN
  • SYN + FIN
  • SYN + DATA with FIN + DATA
  • ...

然后根据未通过的测试,一步步完善逻辑,测试全部通过后再重新简化代码,最终得到解法如下(部分思路见注释):

void TCPReceiver::segment_received(const TCPSegment &seg) {
    TCPHeader header = seg.header();
    if (header.syn && _syn)
        return;
    if (header.syn) {
        _syn = true;
        _isn = header.seqno.raw_value();
    }
    // note that fin flag seg can carry payload
    if (_syn && header.fin)
        _fin = true;
    size_t absolute_seqno = unwrap(header.seqno, WrappingInt32(_isn), _checkpoint);
    _reassembler.push_substring(seg.payload().copy(), header.syn ? 0 : absolute_seqno - 1, header.fin);
    _checkpoint = absolute_seqno;
}

optional<WrappingInt32> TCPReceiver::ackno() const {
    // Return the corresponding 32bit seqno of _reassembler.first_unassembled().
    // Translate from Stream Index to Absolute Sequence Number is simple, just add 1.
    // If outgoing ack is corresponding to FIN
    // (meaning FIN is received and all segs are assembled),
    // add another 1.
    size_t shift = 1;
    if (_fin && _reassembler.unassembled_bytes() == 0)
        shift = 2;
    if (_syn)
        return wrap(_reassembler.first_unassembled() + shift, WrappingInt32(_isn));
    return {};
}

size_t TCPReceiver::window_size() const {
    // equal to first_unacceptable - first_unassembled
    return _capacity - stream_out().buffer_size();
}
查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月8日

计算机教育中缺失的一课 - MIT - L11 - Q&A

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

笔记

OS 学习资料

  • MIT’s 6.828 - 研究生阶段的操作系统课程,带你实现一个 OS
  • 现代操作系统 - Andrew S. Tanenbaum,对各种概念做了系统的讲解
  • FreeBSD的设计与实现( The Design and Implementation of the FreeBSD Operating System ) - 关于FreeBSD OS 不错的资源(注意,FreeBSD OS 不是 Linux)
  • 用 Rust 写操作系统

source script.sh./script.sh

不同点在于哪个会话执行这个命令。 对于 source 命令来说,命令是在当前的bash会话中执行的,因此当 source 执行完毕,对当前环境的任何更改(例如更改目录或是定义函数)都会留存在当前会话中。 单独运行 ./script.sh 时,当前的bash会话将启动新的bash会话(实例),并在新实例中运行命令 script.sh

性能分析工具

  • 最简单但是有效的:在代码中添加打印运行时间的语句,通过二分法逐步定位到花费时间最长的代码段。
  • Valgrind 的 Callgrind 可以让你运行程序并计算所有的时间花费以及所有调用堆栈。然后,它会生成带注释的代码版本,其中包含每行花费的时间。注意它不支持线程。
  • 特定的编程语言可能会有自带的或者特定的第三方的分析工具
  • 用于用户程序内核跟踪的eBPF、低级的性能分析工具 bpftrace:分析系统调用中的等待时间,因为有时代码中最慢的部分是系统等待磁盘读取或网络数据包之类的事件

浏览器插件

数据整理工具

  • 在数据整理一讲中提到的分别针对 JSON 和 HTML 的 jq 和 pup
  • Perl 语言非常擅长处理文本,值得进行学习,但它是一种“Write Only”的语言,因为写出来的代码可读性非常差
  • Vim 也可以用来整理数据,例如利用 Vim 的宏
  • Python 的 pandas 库是整理表格数据(或类似格式)的好工具
  • Pandoc:a universal document converter,可以在各种文档之间进行转换,HTML、Markdown、LaTex、docx、XML 等等
  • R语言(一种有争议的不好的语言)作为一种主要用于统计分析的编程语言,在管道的最后一步(比如画图展示)非常有用,其绘图库 ggplot2非常强大。

Docker 与虚拟机的区别

  • 虚拟机会执行整个的 OS 栈,包括内核(即使这个内核和主机内核相同)
  • 容器与主机分享内核(在Linux环境中,有LXC机制来实现),当然容器内部感知不到,仍像是在使用自己的硬件启动程序
  • 容器的隔离性较弱而且只有在主机运行相同的内核时才能正常工作

    • 例如,如果你在macOS 上运行 Docker,Docker 需要启动 Linux虚拟机去获取初始的 Linux内核,这样的开销仍然很大
  • Docker 是容器的特定实现,它是为软件部署而定制的,有一些奇怪之处,例如

    • 在默认情况下,Docker 容器没有任何形式的持久化存储,关闭之后数据消失,而与之对应虚拟机通常有一个虚拟硬盘文件保存在主机上

如何选择操作系统

  • 可以使用任何 Linux 发行版(Distro)去学习 Linux 与 UNIX 的特性和其内部工作原理
  • 发行版之间的根本区别是发行版如何处理软件包更新

    • Arch Linux 采用滚动更新策略,用了最前沿的软件包(bleeding-edge),但软件可能并不稳定
    • Debian,CentOS 或 Ubuntu LTS 的更新策略要保守得多,因此更加稳定
  • Mac OS 是介于 Windows 和 Linux 之间的一个操作系统

    • Mac OS 是基于BSD 而不是 Linux
  • 另一种值得体验的是 FreeBSD

    • 与 Linux 相比,BSD 生态系统的碎片化程度要低得多,并且说明文档更加友好
  • 作为程序员,你为什么还在用 Windows?除非你开发 Windows 应用程序或需要使用某些 Windows 系统更好支持的功能(例如对游戏的驱动程序支持)(有被冒犯到。。。)
  • 对于双系统,我们认为最有效的是 macOS 的 bootcamp,因为长期来看,任何其他组合都可能会出现问题,尤其是当你结合了其他功能比如磁盘加密

Vim 还是 Emacs

Emacs 不使用 vim 的模式编辑,但是这些功能可以通过 Emacs 插件比如 EvilDoom Emacs 来实现。 Emacs的优点是可以用 Lisp 语言进行扩展(Lisp 比 vim 默认的脚本语言 vimscript 要更好用)。

机器学习应用的技巧

  • 机器学习应用需要进行许多实验,探索数据,可以使用 Shell 轻松快速地搜索这些实验结果,并且以合理的方式汇总。
  • 使用课程中介绍过的数据整理的工具,通过使用 JSON 文件记录实验的所有相关参数,让你的实验结果变得井井有条且可复现。
  • 如果不使用集群提交 GPU 作业,那你应该研究如何使这些过程自动化。

两步验证(2FA)

最简单的情形是可以通过接收手机的 SMS 来实现(尽管 SMS 2FA 存在 已知问题)。我们推荐使用 YubiKey 之类的 U2F 方案。

如何选择浏览器

  • Chrome 的渲染引擎是 Blink,JS 引擎是 V8。
  • Firefox 的渲染引擎是 Gecko,JS 引擎是 SpiderMonkey。
  • 其他浏览器大多都是 Chrome 的变种,用着 Chromium 内核,运行着同样的引擎,例如新版的 Microsoft Edge。至于 Safari 则基于 WebKit(与Blink类似的引擎)。这些浏览器仅仅是更糟糕的 Chrome 版本。
  • Firefox 与 Chrome 的在各方面不相上下,但在隐私方面更加出色。
  • Firefox 正在使用 Rust 重写他们的渲染引擎,名为 Servo。
  • 一款目前还没有完成的叫 Flow 的浏览器,它实现了全新的渲染引擎,有望比现有引擎速度更快。
查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月8日

计算机教育中缺失的一课 - MIT - L10 - 大杂烩

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

笔记

修改键位映射

修改键位映射可以通过软件或者硬件(支持定制固件的键盘)实现。软件可以实现更复杂的修改例如对不同的键盘或软件保存专用的映射配置。

下面是一些修改键位映射的软件:

守护进程(daemon)

  • 在后台保持运行,不需要用户手动运行或者交互
  • 以守护进程运行的程序名一般以 d 结尾
  • SSH 服务端 sshd,用来监听传入的 SSH 连接请求并对用户进行鉴权
  • Linux 中的 systemd(the system daemon),用来配置和运行守护进程

    • 使用 systemctl 命令来与 systemd 交互
    • systemctl enable|disable|start|stop|restart|status
  • 如果只是想定期运行一些程序,可以直接使用 cron。它是一个系统内置的,用来执行定期任务的守护进程。

下面的配置文件使用了 systemd 来运行一个 Python 程序,systemd 配置文件的详细指南可参见 freedesktop.org

# /etc/systemd/system/myapp.service
[Unit]
# 配置文件描述
Description=My Custom App
# 在网络服务启动后启动该进程
After=network.target

[Service]
# 运行该进程的用户
User=foo
# 运行该进程的用户组
Group=foo
# 运行该进程的根目录
WorkingDirectory=/home/foo/projects/mydaemon
# 开始该进程的命令
ExecStart=/usr/bin/local/python3.7 app.py
# 在出现错误时重启该进程
Restart=on-failure

[Install]
# 相当于Windows的开机启动。即使GUI没有启动,该进程也会加载并运行
WantedBy=multi-user.target
# 如果该进程仅需要在GUI活动时运行,这里应写作:
# WantedBy=graphical.target
# graphical.target在multi-user.target的基础上运行和GUI相关的服务

FUSE

用户空间文件系统Filesystem in Userspace,简称FUSE)是一个面向类Unix计算机操作系统的软件接口,它使无特权的用户能够无需编辑内核代码而创建自己的文件系统。目前Linux通过内核模块对此进行支持。

一些有趣的 FUSE 文件系统包括:

  • sshfs:一个将所有文件系统操作都使用 SSH 转发到远程主机,由远程主机处理后返回结果到本地计算机的虚拟文件系统。这个文件系统里的文件虽然存储在远程主机,对于本地计算机上的软件而言和存储在本地别无二致
  • rclone:将 Dropbox、Google Drive、Amazon S3、或者 Google Cloud Storage 一类的云存储服务挂载为本地文件系统
  • gocryptfs:覆盖在加密文件上的文件系统。文件以加密形式保存在磁盘里,但该文件系统挂载后用户可以直接从挂载点访问文件的明文
  • kbfs:分布式端到端加密文件系统。在这个文件系统里有私密(private),共享(shared),以及公开(public)三种类型的文件夹
  • borgbackup:方便用户浏览删除重复数据后的压缩加密备份

备份

  • 复制存储在同一个磁盘上的数据不是备份,因为这个磁盘是一个单点故障(single point of failure)
  • 同步方案不是备份

    • 如 Dropbox 或者 Google Drive,当数据在本地被抹除或者损坏,同步方案可能会把这些“更改”同步到云端。
    • RAID 这样的磁盘镜像方案也不是备份。它不能防止文件被意外删除、损坏、或者被勒索软件加密。
  • 不要盲目信任备份方案。用户应该经常检查备份是否可以用来恢复数据。

    • 云端应用的重大发展使得我们很多的数据只存储在云端。但用户应该有这些数据的离线备份。

有效备份方案的核心特性:

  • 版本控制
  • 删除重复数据
  • 安全性(别人需要有什么信息或者工具才可以访问或者完全删除你的数据及备份)

该课程2019年关于备份的 课堂笔记

API(应用程序接口)

  • 大多数线上服务提供的 API 具有类似的格式。它们的结构化 URL 通常使用 api.service.com 作为根路径。

  • 通常这些返回都是 JSON 格式,你可以使用 jq 等工具来选取需要的部分。
  • 有些需要认证的 API 通常要求用户在请求中加入某种私密令牌(secret token)来完成认证。大多数 API 都会使用 OAuth
  • IFTTT 这个网站可以将很多 API 整合在一起,让某 API 发生的特定事件触发在其他 API 上执行的任务。IFTTT 的全称 If This Then That 足以说明它的用法,比如在检测到用户的新推文后,自动发布在其他平台。

常见命令行标志参数及模式

  • --help-h 或者类似的标志参数(flag)来显示简略用法
  • 会造成不可撤回操作的工具一般会提供“空运行”(dry run)标志参数和“交互式”(interactive)标志参数
  • 会造成破坏性结果的工具一般默认进行非递归的操作,但是支持使用“递归”(recursive)标志函数(通常是 -r
  • --version 或者 -V 标志参数可以让工具显示它的版本信息
  • --verbose 或者 -v 标志参数来输出详细的运行信息。多次使用这个标志参数,比如 -vvv,可以让工具输出更详细的信息(经常用于调试)
  • --quiet 标志参数来抑制除错误提示之外的其他输出。
  • 使用 - 代替输入或者输出文件名意味着工具将从标准输入(standard input)获取所需内容,或者向标准输出(standard output)输出结果,可以参考之前的笔记:计算机教育中缺失的一课 - MIT - L4 - 数据整理
  • 有的时候你可能需要向工具传入一个 看上去 像标志参数的普通参数,这时候你可以使用特殊参数 -- 让某个程序 停止处理-- 后面出现的标志参数以及选项(以 - 开头的内容):

    • rm -- -r 会让 rm-r 当作文件名;
    • ssh machine --for-ssh -- foo --for-foo-- 会让 ssh 知道 --for-foo 不是 ssh 的标志参数。

窗口管理器

大部分操作系统默认的窗口管理方式都是“拖拽”式的,这被称作堆叠式(floating/stacking)管理器。另外一种管理器是平铺式(tiling)管理器,其使用逻辑和 tmux 管理终端窗口的方式类似(参考之前的笔记:计算机教育中缺失的一课 - MIT - L5 - 命令行环境),可以让我们在完全不使用鼠标的情况下使用键盘切换、缩放、以及移动窗口。

VPN

关于这一部分,课程的 Lecture Note 写得已经十分简洁,直接摘录下来。

VPN 现在非常火,但我们不清楚这是不是因为一些好的理由。你应该了解 VPN 能提供的功能和它的限制。使用了 VPN 的你对于互联网而言,最好的情况下也就是换了一个网络供应商(ISP)。所有你发出的流量看上去来源于 VPN 供应商的网络而不是你的“真实”地址,而你实际接入的网络只能看到加密的流量。

虽然这听上去非常诱人,但是你应该知道使用 VPN 只是把原本对网络供应商的信任放在了 VPN 供应商那里——网络供应商 能看到的 ,VPN 供应商 也都能看到 。如果相比网络供应商你更信任 VPN 供应商,那当然很好。反之,则连接VPN的价值不明确。机场的不加密公共热点确实不可以信任,但是在家庭网络环境里,这个差异就没有那么明显。

你也应该了解现在大部分包含用户敏感信息的流量已经被 HTTPS 或者 TLS 加密。这种情况下你所处的网络环境是否“安全”不太重要:供应商只能看到你和哪些服务器在交谈,却不能看到你们交谈的内容。

这一切的大前提都是“最好的情况”。曾经发生过 VPN 提供商错误使用弱加密或者直接禁用加密的先例。另外,有些恶意的或者带有投机心态的供应商会记录和你有关的所有流量,并很可能会将这些信息卖给第三方。找错一家 VPN 经常比一开始就不用 VPN 更危险。

MIT 向有访问校内资源需求的成员开放自己运营的 VPN。如果你也想自己配置一个 VPN,可以了解一下 WireGuard 以及 Algo

Markdown

Markdown 是一个轻量化的标记语言(markup language),也致力于将人们编写纯文本时的一些习惯标准化。

Hammerspoon (macOS 桌面自动化)

Hammerspoon 是面向 macOS 的一个桌面自动化框架。它允许用户编写和操作系统功能挂钩的 Lua 脚本,从而与键盘、鼠标、窗口、文件系统等交互。

开机引导以及 Live USB

在计算机启动时,BIOS 或者 UEFI 会在加载操作系统之前对硬件系统进行初始化,这被称为引导(booting)。在 BIOS 菜单中你可以对硬件相关的设置进行更改,也可以在引导菜单中选择从硬盘以外的其他设备加载操作系统——比如 Live USB。

Live USB 是包含了完整操作系统的闪存盘。Live USB 的用途非常广泛,包括:

  • 作为安装操作系统的启动盘;
  • 在不将操作系统安装到硬盘的情况下,直接运行 Live USB 上的操作系统;
  • 对硬盘上的相同操作系统进行修复;
  • 恢复硬盘上的数据。

Live USB 通过在闪存盘上 写入 操作系统的镜像制作,写入不是单纯的往闪存盘上复制 .iso 文件。可以使用 UNetbootinRufusUltraISO 等 Live USB 写入工具制作。

虚拟技术

虚拟机(Virtual Machine)以及如容器化(containerization)(亦称操作系统层虚拟化)等工具可以帮助你模拟一个包括操作系统的完整计算机系统。

  • Vagrant:一个构建和配置虚拟开发环境的工具。它支持用户在配置文件中写入比如操作系统、系统服务、需要安装的软件包等描述,然后使用 vagrant up 命令在各种环境(VirtualBox,KVM,Hyper-V等)中启动一个虚拟机。
  • Docker:一个使用容器化概念的与 Vagrant 类似的工具,在后端服务的部署中应用广泛。
  • VPS(虚拟专用服务器):将一台服务器分割成多个虚拟专用服务器的服务

交互式记事本编程

交互式记事本可以帮助开发者进行与运行结果交互等探索性的编程。现在最受欢迎的交互式记事本环境大概是 Jupyter。它的名字来源于所支持的三种核心语言:Julia、Python、R。Wolfram Mathematica 是另外一个常用于科学计算的优秀环境。
查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月8日

计算机教育中缺失的一课 - MIT - L9 - 安全和密码学

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

笔记

)(Entropy) 度量了不确定性并可以用来决定密码的强度。

熵的单位是 比特 。对于一个均匀分布的随机离散变量,熵等于 log_2(所有可能的个数,即 n)。 扔一次硬币的熵是1比特。掷一次(六面)骰子的熵大约为2.58比特。

使用多少比特的熵取决于应用的威胁模型。大约40比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制)。而对于离线穷举攻击(主要受限于计算速度),一般需要更强的密码 (比如80比特或更多)。

散列函数

密码散列函数 (Cryptographic hash function) 可以将任意大小的数据映射为一个固定大小的输出。散列函数具有如下特性:

  • 确定性(deterministic):对于不变的输入永远有相同的输出。
  • 不可逆性(non-invertible):对于hash(m) = h,难以通过已知的输出h来计算出原始输入m
  • 目标碰撞抵抗性/弱无碰撞(target collision resistant):对于一个给定输入m_1,难以找到m_2 != m_1hash(m_1) = hash(m_2)
  • 碰撞抵抗性/强无碰撞(collision resistant):难以找到一组满足hash(m_1) = hash(m_2)的输入m_1, m_2(该性质严格强于目标碰撞抵抗性)。

SHA-1是Git中使用的一种散列函数,Linux 下有 sha1sum 工具。

虽然SHA-1还可以用于特定用途,但它已经不再被认为是一个强密码散列函数。参照密码散列函数的生命周期这个表格了解一些散列函数是何时被发现弱点及破解的。

密码散列函数的应用

  • Git中的内容寻址存储(Content addressed storage):散列函数是一个宽泛的概念(存在非密码学的散列函数),那么Git为什么要特意使用密码散列函数?

    • 普通的散列函数没有无碰撞性,Git 使用密码散列函数,来确保分布式版本控制系统中的两个不同数据不会有相同的摘要信息(例如两个内容不同的 commit 不应该有相同的哈希值)。
  • 文件的信息摘要(Message digest):例如下载文件时,对比下载下来的文件的哈希值和官方公布的哈希值是否相同来判断文件是否损坏或者被篡改。
  • 承诺机制(Commitment scheme):假设你要猜我在脑海中想的一个随机数字,我先告诉你该数字的哈希值,然后你猜数字,我再告诉你正确答案,看你是否猜对,这时你可以通过先前公布的哈希值来确认我没有作弊。

密钥生成函数

密钥生成函数 (Key Derivation Functions)与密码散列函数类似,用以产生一个固定长度的密钥。但是为了对抗穷举法攻击,密钥生成函数通常较慢。

密码生成函数的应用

  • 将其结果作为其他加密算法的密钥,例如对称加密算法
  • 数据库中保存的用户密码为密文

    • 针对每个用户随机生成一个),并存储盐,以及密钥生成函数对连接了盐的明文密码生成的哈希值 KDF(password + salt)
    • 在验证登录请求时,使用输入的密码连接存储的盐重新计算哈希值KDF(input + salt),并与存储的哈希值对比。
    • (Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为“加盐”。
    • 在大部分情况,盐是不需要保密的。
    • 通常情况下,当字段经过散列处理,会生成一段散列值,而散列后的值一般是无法通过特定算法得到原始字段的。但是某些情况,比如一个大型的彩虹表,通过在表中搜索该SHA-1值,很有可能在极短的时间内找到该散列值对应的真实字段内容。
    • 加盐可以避免用户的短密码被彩虹表破解,也可以保护在不同网站使用相同密码的用户。

对称加密

keygen() -> key  (这是一个随机方法,例如使用 KDF(passphrase))

encrypt(plaintext: array<byte>, key) -> array<byte>  (输出密文)
decrypt(ciphertext: array<byte>, key) -> array<byte>  (输出明文)

加密方法encrypt()输出的密文ciphertext很难在不知道key的情况下得出明文plaintext

AES 是现在常用的一种对称加密系统。在 Linux 下可以使用 openssl 工具:

openssl aes-256-cbc -salt -in {源文件名} -out {加密文件名}
openssl aes-256-cbc -d -in {加密文件名} -out {解密文件名}

非对称加密

非对称加密的“非对称”代表在其环境中,使用两个具有不同功能的密钥: 一个是私钥(private key),不向外公布;另一个是公钥(public key),公布公钥不像公布对称加密的共享密钥那样可能影响加密体系的安全性。

keygen() -> (public key, private key)  (这是一个随机方法)

encrypt(plaintext: array<byte>, public key) -> array<byte>  (输出密文)
decrypt(ciphertext: array<byte>, private key) -> array<byte>  (输出明文)

sign(message: array<byte>, private key) -> array<byte>  (生成签名)
verify(message: array<byte>, signature: array<byte>, public key) -> bool  (验证签名是否是由和这个公钥相关的私钥生成的)

非对称的加密/解密方法和对称的加密/解密方法有类似的特征。
信息在非对称加密中使用 公钥 加密, 且输出的密文很难在不知道 私钥 的情况下得出明文。

在不知道 私钥 的情况下,不管需要签名的信息为何,很难计算出一个可以使 verify(message, signature, public key) 返回为真的签名。

非对称加密的应用

  • PGP电子邮件加密:用户可以将所使用的公钥在线发布,比如:PGP密钥服务器或 Keybase。任何人都可以向他们发送加密的电子邮件。
  • 聊天加密:像 SignalTelegramKeybase 使用非对称密钥来建立私密聊天。
  • 软件签名:Git 支持用户对提交(commit)和标签(tag)进行GPG签名。任何人都可以使用软件开发者公布的签名公钥验证下载的已签名软件。

密钥分发

非对称加密面对的主要挑战是,如何分发公钥并对应现实世界中存在的人或组织。

  • Signal的信任模型:信任用户第一次使用时给出的身份(trust on first use),支持线下(out-of-band)面对面交换公钥(Signal里的safety number)。
  • PGP使用的是信任网络
  • Keybase主要使用社交网络证明 (social proof)

案例分析

  • 密码管理器

  • 两步验证(2FA)(多重身份验证 MFA)

    • 要求用户同时使用密码(“你知道的信息”)和一个身份验证器(“你拥有的物品”,比如YubiKey)来消除密码泄露或者钓鱼攻击的威胁。
  • 全盘加密

    • 对笔记本电脑的硬盘进行全盘加密是防止因设备丢失而信息泄露的简单且有效方法。
    • Linux的 cryptsetup + LUKS
    • Windows的 BitLocker
    • macOS的 FileVault
  • 聊天加密

    • 获取联系人的公钥非常关键。为了保证安全性,应使用线下方式验证用户公钥,或者信任用户提供的社交网络证明。
  • SSH

    • ssh-keygen 命令会生成一个非对称密钥对。公钥最终会被分发,它可以直接明文存储。但是为了防止泄露,私钥必须加密存储。
    • ssh-keygen 命令会提示用户输入一个密码,并将它输入 KDF 产生一个密钥。最终,ssh-keygen 使用对称加密算法和这个密钥加密私钥。
    • 当服务器已知用户的公钥(存储在.ssh/authorized_keys文件中),尝试连接的客户端可以使用非对称签名来证明用户的身份——这便是挑战应答方式。 简单来说,服务器选择一个随机数字发送给客户端。客户端使用用户私钥对这个数字信息签名后返回服务器。服务器随后使用保存的用户公钥来验证返回的信息是否由所对应的私钥所签名。这种验证方式可以有效证明试图登录的用户持有所需的私钥。

课后练习

  1. Entropy = log_2(100000^5) = 83
  2. Entropy = log_2((26+26+10)^8) = 48
  3. 第一个更强。
  4. 分别需要 31.7 万亿年和 692 年。

非对称加密

  1. ssh-keygen -r ed25519 -o -C "your_email"
    生成 SSH 公钥
    How To Set Up SSH Keys
  2. How To Use GPG to Encrypt and Sign Messages
    GPG入门教程
  3. 给Anish发送一封加密的电子邮件(Anish的公钥)。
  4. git commit -S命令签名一个Git commit
    git show --show-signature命令验证 commit 的签名
    git tag -s命令签名一个Git标签
    git tag -v命令验证标签的签名
    对提交签名
查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月7日

计算机教育中缺失的一课 - MIT - L8 - 元编程

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

思否主页:https://segmentfault.com/u/wi...

笔记

元编程通常又指 用于操作程序的程序,讲座中讨论的更多是关于开发流程

构建系统

“构建系统”帮助我们执行一系列的“构建过程”。构建过程包括:目标(targets),依赖(dependencies),规则(rules)。您必须告诉构建系统您具体的构建目标,系统的任务则是找到构建这些目标所需要的依赖,并根据规则构建所需的中间产物,直到最终目标被构建出来。

理想的情况下,如果目标的依赖没有发生改动,并且我们可以从之前的构建中复用这些依赖,那么与其相关的构建规则并不会被执行。

make 是最常用的构建系统之一,您会发现它通常被安装到了几乎所有基于UNIX的系统中。make 的教程可以参考阮一峰的这篇文章:Make 命令教程

其他常见的构建系统/工具:

  • C 与 C++:Cmake,可以参考 CMake 入门实战
  • Java:Maven,Ant,Gradle
  • 前端开发:Grunt,Gulp,Webpack
  • Ruby:Rake
  • Rust:Cargo

依赖管理

软件仓库

  • Ubuntu:可以通过 apt 这个工具来访问 Ubuntu 软件包仓库
  • CentOS,Redhat:通过 yum 这个工具来访问软件仓库
  • Archlinux/Manjaro:通过 pacman 工具访问 Archlinux 软件仓库和 Arch 用户软件仓库(AUR,Arch User Repository)
  • Ruby:通过 gem 工具访问 RubyGems
  • Python:通过 pip 工具访问 Pypi

版本号

不同项目所用的版本号其具体含义并不完全相同,但是一个相对比较常用的标准是语义版本号,这种版本号具有不同的语义,它的格式是这样的:major.minor.patch(主版本号.次版本号.补丁号)。相关规则有:

  • 如果新的版本没有改变 API,请将补丁号递增;
  • 如果您添加了 API 并且该改动是向后兼容的,请将次版本号递增;
  • 如果您修改了 API 但是它并不向后兼容,请将主版本号递增。

这样做有很多好处,例如如果我们的项目是基于您的项目构建的,那么只要最新版本的主版本号只要没变就是安全的,次版本号不低于之前我们使用的版本即可。换句话说,如果我依赖的版本是1.3.7,那么使用1.3.81.6.1,甚至是1.3.0都是可以的。如果版本号是 2.2.4 就不一定能用了,因为它的主版本号增加了。

持续集成系统

持续集成,或者叫做 CI 是一种雨伞术语(umbrella term),它指的是那些“当您的代码变动时,自动运行的东西”,可以认为是一种云端构建系统。

市场上有很多提供各式各样 CI 工具的公司,例如 Travis CI、Azure Pipelines 和 GitHub Actions。

它们使用方法大同小异:在代码仓库中添加一个文件(recipe),在其中编写规则,规则包括 events 和 actions。

最常见的规则是:如果有人提交代码,执行测试套。当这个事件被触发时,CI 提供方会启动一个(或多个)虚拟机,执行您制定的规则,并且通常会记录下相关的执行结果。您可以进行某些设置,这样当测试套失败时您能够收到通知或者当测试全部通过时,您的仓库主页会显示一个徽标。

Github 还有一个维护依赖关系的 CI 工具 Dependabot

GitHub Pages 是一个很好的例子。Pages 在每次master有代码更新时,会执行 Jekyll 博客软件,然后使您的站点可以通过某个 GitHub 域名来访问。对于我们来说这些事情太琐碎了,我现在我们只需要在本地进行修改,然后使用 git 提交代码,发布到远端。CI 会自动帮我们处理后续的事情。

测试

  • 测试套(Test suite):所有测试的统称
  • 单元测试(Unit test):一个“微型测试”,用于对某个封装的特性进行测试
  • 集成测试(Integration test): 一个“宏观测试”,针对系统的某一大部分进行,测试其不同的特性或组件是否能协同工作。
  • 回归测试(Regression test):用于保证之前引起问题的 bug 不会再次出现
  • 模拟(Mocking): 使用一个假的实现来替换函数、模块或类型,屏蔽那些和测试不相关的内容。例如,您可能会“模拟网络连接” 或 “模拟硬盘”

课后练习

习题 1

一些有用的 make 构建目标(例如本题用到了 phony)。

.PHONY: clean
clean:
      git ls-files -o | xargs rm
      # 这样还会删掉 gitignore 中的文件,例如一些编辑器配置文件
      # 此题也可以这样做
      # rm plot-*.png
      # rm paper.pdf

习题 3

#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.

# Redirect output to stderr.
exec 1>&2

if (! make)
then
    cat <<\EOF
Error: make failed.

Excuting 'make paper.pdf'.
EOF
    make paper.pdf
    exit 1
fi

习题 4

基于 GitHub Pages 创建任意一个可以自动发布的页面。添加一个GitHub Action 到该仓库,对仓库中的所有 shell 文件执行 shellcheck(方法之一)。

习题 5

构建属于您的 GitHub action,对仓库中所有的.md文件执行proselintwrite-good,在您的仓库中开启这一功能,提交一个包含错误的文件看看该功能是否生效。

查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月5日

计算机教育中缺失的一课 - MIT - L7 - 调试及性能分析

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

笔记

调试

打印调试法与日志

日志相比临时添加打印语句有如下优势:

  • 您可以将日志写入文件、socket 或者甚至是发送到远端服务器而不仅仅是标准输出;
  • 日志可以支持严重等级(例如 INFO, DEBUG, WARN, ERROR等),这使您可以根据需要过滤日志;
  • 对于新发现的问题,很可能您的日志中已经包含了可以帮助您定位问题的足够的信息。

这里 是课堂演示的包含日志的 Python 程序。

lsgrep 这样的程序会使用 ANSI escape codes,它是一系列的特殊字符,可以使您的 shell 改变输出结果的颜色。

#!/usr/bin/env bash
for R in $(seq 0 20 255); do
    for G in $(seq 0 20 255); do
        for B in $(seq 0 20 255); do printf "e[38;2;${R};${G};${B}m█e[0m";
        done
    done
done

第三方日志系统

  • 程序的日志通常存放在 /var/log
  • 大多数的 Linux 系统都会使用 systemd,这是一个系统守护进程,它会控制您系统中的很多东西,例如哪些服务应该启动并运行
  • systemd 会将日志以某种特殊格式存放于 /var/log/journal,您可以使用 journalctl 命令显示这些消息
  • macOS 系统中是 /var/log/system.log,但是有更多的工具会使用系统日志,它的内容可以使用 log show 显示
  • 对于大多数的 UNIX 系统,您也可以使用dmesg 命令来读取内核的日志
  • 使用 logger 这个 shell 程序将日志加入到系统日志中
  • 一些像 lnav 这样的工具,它为日志文件提供了更好的展现和浏览方式

调试器

调试器可以:

  • 当到达某一行时将程序暂停;
  • 一次一条指令地逐步执行程序;
  • 程序崩溃后查看变量的值;
  • 满足特定条件时暂停程序;
  • 其他高级功能。

常见调试器有:

  • pdb:Python 的调试器
  • ipdb:一种增强型的 pdb ,它使用IPython 作为 REPL并开启了 tab 补全、语法高亮、更好的回溯和更好的内省,同时还保留了pdb 模块相同的接口
  • gdb ( 以及它的改进版 pwndbg) 和 lldb:C 和类 C 语言的调试器,还可以探索任意进程及其机器状态:寄存器、堆栈、程序计数器等

专门工具

  • 追踪普通二进制程序执行的系统调用:strace(Linux)和 dtrace(macOS 和 BSD),一个叫做 dtruss 的封装使 dtruss 具有和 strace (更多信息参考 这里)类似的接口
  • 网络数据包分析工具:tcpdumpWireshark
  • web 开发:Chrome/Firefox 的开发者工具

静态分析

静态分析 工具将程序的源码作为输入然后基于编码规则对其进行分析并对代码的正确性进行推理。

大多数的编辑器和 IDE 都支持在编辑界面显示这些工具(还有风格检查或安全检查)的分析结果、高亮有警告和错误的位置。这个过程通常称为 code linting

  • 静态分析工具

    • pyflakes:Python 的静态分析工具
    • mypy:另外一个 Python 静态分析工具,它可以对代码进行类型检查
    • shellcheck:shell 脚本的静态分析工具,在 shell 工具那一节课介绍过
  • 风格检查和安全检查工具

    • pylint, pep8, black:都是 Python 的风格检查工具
    • gofmt:Go 的风格检查工具
    • rustfmt:Rust 的风格检查工具
    • prettier:JavaScript, HTML 和 CSS 的风格检查工具
    • bandit:Python 的安全检查工具
  • Vim 的 code linting 插件

性能分析

计时

最常见的做法是打印两处代码之间的时间差来获得执行时间(wall clock time),例如使用 Python 的 time模块。不过,执行时间也可能会误导您,因为您的电脑可能也在同时运行其他进程,也可能在此期间发生了等待。通常来说,用户时间 + 系统时间代表了您的进程所消耗的实际 CPU (更详细的解释可以参照这篇文章)。

  • 真实时间 - 从程序开始到结束流失掉的真实时间,包括其他进程的执行时间以及阻塞消耗的时间(例如等待 I/O或网络);
  • User - CPU 执行用户代码所花费的时间;
  • Sys - CPU 执行系统内核代码所花费的时间。

例如,试着执行一个用于发起 HTTP 请求的命令并在其前面添加 time 前缀。网络不好的情况下您可能会看到下面的输出结果。请求花费了 2s 才完成,但是进程仅花费了 15ms 的 CPU 用户时间和 12ms 的 CPU 内核时间。

$ time curl https://missing.csail.mit.edu &> /dev/null`
real    0m2.561s
user    0m0.015s
sys     0m0.012s

性能分析工具(profilers)

CPU

How do Ruby & Python profilers work?

大多数情况下,当人们提及性能分析工具的时候,通常指的是 CPU 性能分析工具。以 Python 的性能分析工具举例:

  • 追踪分析器(tracing)

    • cProfile:追踪函数调用耗时。需要注意的是它显示的是每次函数调用的时间。看上去可能快到反直觉,尤其是如果您在代码里面使用了第三方的函数库,因为内部函数调用也会被看作函数调用
    • line_profiler:行分析器。
  • 采样分析器(sampling)(周期性地监测您的程序并记录程序堆栈)

内存
  • 对于手动管理内存的语言可能存在内存泄漏问题,例如 C、C++,可以使用类似 Valgrind 这样的工具来检查
  • 对于有 GC 的语言,例如 Python、Java、JavaScript,内存分析器也是很有用的,因为对于某个对象来说,只要有指针还指向它,那它就不会被回收,可以使用 memory-profiler 来对 Python 代码进行内存分析

事件分析

前面提到 strace 可用以追踪程序执行的系统调用,perf 命令可以追踪报告特定的系统事件,例如不佳的缓存局部性(poor cache locality)、大量的页错误(page faults)或活锁(livelocks)。下面是关于常见命令的简介:

  • perf list - 列出可以被 pref 追踪的事件;
  • perf stat COMMAND ARG1 ARG2 - 收集与某个进程或指令相关的事件;
  • perf record COMMAND ARG1 ARG2 - 记录命令执行的采样信息并将统计数据储存在perf.data中;
  • perf report - 格式化并打印 perf.data 中的数据。

可视化

对于采样分析器来说,常见的显示 CPU 分析数据的形式是 火焰图,火焰图会在 Y 轴显示函数调用关系,并在 X 轴显示其耗时的比例。

FlameGraph

调用图和控制流图可以显示子程序之间的关系,它将函数作为节点并把函数调用作为边。将它们和分析器的信息(例如调用次数、耗时等)放在一起使用时,调用图会变得非常有用,它可以帮助我们分析程序的流程。 在 Python 中您可以使用 pycallgraph 来生成这些图片。

资源监控

  • 通用监控

    • htoptop 的改进版,常用快捷键有:<F6> 进程排序、 t 显示树状结构和 h 打开或折叠线程
    • glances,实现类似但是用户界面更好
    • 如果需要, dstat,合并测量进程,可以实时地计算不同子系统资源的度量数据,例如 I/O、网络、 CPU 利用率、上下文切换等等
  • I/O 操作

    • iotop 可以显示实时 I/O 占用信息而且可以非常方便地检查某个进程是否正在执行大量的磁盘读写操作
  • 磁盘使用

    • df 可以显示每个分区的信息
    • du 可以显示当前目录下每个文件的磁盘使用情况( disk usage)。-h 选项可以使命令以对人类更加友好的格式显示数据
    • ncdu是一个交互性更好的 du ,可以在不同目录下导航、删除文件和文件夹
  • 内存使用

    • free 可以显示系统当前空闲的内存。内存,也可以使用 htop 这样的工具来显示
  • 打开文件

    • lsof 可以列出被进程打开的文件信息。 当我们需要查看某个文件是被哪个进程打开的时候,这个命令非常有用;
  • 网络连接和配置

    • ss 能帮助我们监控网络包的收发情况以及网络接口的显示信息,ss 常见的一个使用场景是找到端口被进程占用的信息
    • ip 命令可以显示路由、网络设备和接口信息
    • netstatifconfig 这两个命令已经被前面那些工具所代替了
  • 网络使用

    • nethogsiftop 是非常好的用于对网络占用进行监控的交互式命令行工具

如果您希望测试一下这些工具,您可以使用 stress 命令来为系统人为地增加负载。

专用工具

hyperfine 这样的命令行可以帮您快速进行基准测试(benchmark)。例如,我们在 shell 工具和脚本那一节课中我们推荐使用 fd 来代替 find。我们这里可以用 hyperfine 来比较一下它们。

$ hyperfine --warmup 3 'fd -e jpg' 'find . -iname "*.jpg"'

浏览器(例如 Chrome 和 Firefox)也包含了很多不错的性能分析工具,可以用来分析页面加载,让我们可以搞清楚时间都消耗在什么地方(加载、渲染、脚本等等)。

课后习题

调试

习题 2

学习 这份pdb 实践教程并熟悉相关的命令。更深入的信息您可以参考这份教程。

习题 3

给 Vim 安装 ale 插件后:

yGL6FP.png

修改后:

for f in $(glob '*.m3u')
do
  grep -qi "hq.*mp3" "$f" \
    && echo "Playlist $f contains a HQ file in mp3 format"
done

习题 4

阅读 可逆调试 并尝试创建一个可以工作的例子(使用 rrRevPDB)。

性能分析

习题 2

简单递归的调用图:

yGjUzT.png

带备忘录的递归的调用图:

yGvZm4.png

习题 3

yGv43V.png

习题 4

执行 stress -c 3 时,在 htop 中可以看到,有三个 CPU 核心正在满负荷工作(不一定是 0, 1, 2 这三个)。

执行 taskset --cpu-list 0,2 stress -c 3 时,在 htop 中可以看到,stress 程序只占用了 0, 2 两个 CPU,这是 taskset 命令的参数指定的。如果改为 taskset --cpu-list 0-2 stress -c 3,则将占用 0, 1, 2 三个 CPU。

cgroups来实现相同的操作:参考 Cgroup限制cpu使用用 cgroups 管理 cpu 资源

查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月4日

计算机教育中缺失的一课 - MIT - L6 - 版本控制 (Git)

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

笔记

Git 的数据模型

Git 通过一系列快照来管理其历史记录。快照则是被追踪的最顶层的树。可以认为 git commit 会创建一个快照。

type object = blob | tree | commit

// 文件就是一组数据
type blob = array<byte>

// 一个包含文件和目录的目录
type tree = map<string, tree | file>

// 每个提交都包含一个父辈,元数据和顶层树
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

// 还有引用(reference),比如
// HEAD, master, origin/HEAD, origin/master
// 都是引用
// 引用是指向提交的指针,与对象不同的是,它是可变的(引用可以被更新,指向新的提交)

实际上,Git 在储存数据时,所有的对象都会基于它们的SHA-1 hash进行寻址。Blobs、trees 和 commits 都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。例如,上面为代码中的 parent: array<commit> 其实际上不是一个 commit 数组,而是一个哈希值数组,这些哈希值指向真正的对象,也就是一些 commits。

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

例如,git cat-file -p 698281b( 698281b 是某个tree,也就是某个文件夹的哈希值的一部分前缀)的结果是:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo 

git cat-file -p 4448adb( 4448adb 是baz.txt 的哈希值的一部分前缀)的结果即为 baz.txte 的内容。

Git 的命令行接口

历史:

  • git log --all --graph --decorate --oneline:可视化历史记录(有向无环图),zsh 的 git 插件定义了很多别名,比如该命令的别名是 gloga,去掉 --all 的别名是 glog
  • git diff <filename>:显示与上一次提交之间的差异
  • git diff <old-revision> [<new-revision>] <filename>:显示某个文件两个版本之间的差异,new-revision 默认是 HEAD
  • git diff --cached <filename>:不加 cached 标识的 diff 的意思是显示尚未暂存的改动,加了之后是查看已暂存的将要添加到下次提交里的内容

修改、撤销和合并

  • git add -p:交互式暂存,例如交互过程中可以按 s 键进行 split,对文件中各个地方的改动分别选择暂存与否
  • git checkout -- <file>:丢弃(尚未暂存的)修改
  • git reset [<tree-ish>] <file>:取消暂存,把文件从暂存区放回工作区,<tree-ish> 默认为 HEAD
  • git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [<commit>]:(见下图)撤销 commit,把 commit 放回暂存区(soft),或放回工作区(mixed),或丢弃(hard),本质是对 HEAD 的移动
  • git reset [--soft | --mixed [-N] | --hard] HEAD^,上一条的特例,比较常用
  • git rebase <branch>:在一个过时的分支上面开发的时候,执行 rebase 以此同步 master 分支最新变动
  • git rebase -i HEAD~n:交互式变基,可用于修改 commit 信息,合并 commit 等等
  • git mergetool:使用工具来处理合并冲突
  • git stash:把工作区暂存起来,允许你切换到其他分支,博主有时候在错误的分支上进行了修改,会用这个命令把修改暂存起来,然后换到正确的工作分支后使用 git stash pop

y1zWCV.png

远端操作

  • git clone --shallow:克隆仓库,但是不包括版本历史信息
  • git remote add <name> <url>:添加一个远端
  • git push <remote> <local branch>:<remote branch>:将对象传送至远端并更新远端引用
  • git branch --set-upstream-to=<remote>/<remote branch>:创建本地和远端分支的关联关系

其他

  • .gitignore: 指定 故意不追踪的文件
  • git blame:查看最后修改某行的人
  • git bisect:通过二分查找搜索历史记录
  • git init --bare:几乎用不到,在课程视频中,讲师在某一个空文件夹中使用该命令,将该文件夹作为 remote,然后将一个已有的仓库 push 到该文件夹
  • git config --global core.excludesfile ~/.gitignore_global:在 ~/.gitignore_global 中创建全局忽略规则
  • Removing sensitive data from a repository

杂项

  • 图形用户界面: Git 的 图形用户界面客户端 有很多,但是我们自己并不使用这些图形用户界面的客户端,我们选择使用命令行接口
  • Shell 集成: 将 Git 状态集成到您的 shell 中会非常方便。(zsh,bash)。Oh My Zsh这样的框架中一般以及集成了这一功能
  • 编辑器集成: 和上面一条类似,将 Git 集成到编辑器中好处多多。fugitive.vim 是 Vim 中集成 GIt 的常用插件
  • 工作流:我们已经讲解了数据模型与一些基础命令,但还没讨论到进行大型项目时的一些惯例 ( 有很多不同的处理方法)
  • GitHub: Git 并不等同于 GitHub。 在 GitHub 中您需要使用一个被称作拉取请求(pull request)的方法来向其他项目贡献代码
  • Other Git 提供商: GitHub 并不是唯一的。还有像GitLabBitBucket这样的平台。

资源

课后习题

习题 2

是谁最后修改来 README.md文件?

$ git log --all -n1 --pretty=format:"%an" README.md

最后一次修改 _config.yml 文件中 collections: 行时的提交信息是什么?

$ git blame _config.yml | grep "collections:" | head -n1 | awk '{print $1}' | sed -E "s/\^//" | xargs git show

# OR

$ git blame _config.yml | grep "collections:" | head -n1 | awk '{print $1}' | sed -E "s/\^//" | xargs git log -n1 --pretty=format:"%s%n%n%b"
查看原文

赞 0 收藏 0 评论 0

wine99 发布了文章 · 2月3日

计算机教育中缺失的一课 - MIT - L5 - 命令行环境

https://missing.csail.mit.edu/
https://missing-semester-cn.g...
https://www.bilibili.com/vide...

笔记

任务控制

shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。就这一点而言,信号是一种软件中断。

结束进程

  • See man signal for reference
  • kill: sends signals to a process; default is TERM
  • SIGINT: ^C; interrupt program; terminate process
  • SIGQUIT: ^\; quit program
  • SIGKILL: terminate process; kill program; cannot be captured by process and will always terminate immediately

    • Can result in orphaned child processes
  • SIGSTOP: pause a process

    • SIGTSTP: ^Z; terminal stop
  • SIGHUP: terminal line hangup; terminate process; will be sent when terminal is closed

    • Use nohup to avoid
  • SIGTERM: signal requesting graceful process exit

    • To send this signal: kill -TERM <pid>

下面这个 Python 程序向您展示了捕获信号 SIGINT 并忽略它的基本操作,它并不会让程序停止。为了停止这个程序,我们需要使用 SIGQUIT 信号。

#!/usr/bin/env python import signal, time

def handler(signum, time):
    print("nI got a SIGINT, but I am not stopping")

signal.signal(signal.SIGINT, handler)
i = 0
while True:
    time.sleep(.1)
    print("r{}".format(i), end="")
    i += 1 
$ python sigint.py
24^C
I got a SIGINT, but I am not stopping
26^C
I got a SIGINT, but I am not stopping
30^\[1]    39913 quit       python sigint.py

暂停和后台执行进程

使用 fgbg 命令恢复暂停的工作。它们分别表示在前台继续或在后台继续。

jobs 命令会列出当前终端会话中尚未完成的全部任务。可以使用 pid 引用这些任务(也可以用 pgrep 找出 pid)。也可以使用百分号 + 任务编号(jobs 会打印任务编号)来选取该任务。如果要选择最近的一个任务,可以使用 $! 这一特殊参数。

命令中的 & 后缀可以让命令在直接在后台运行,不过它此时还是会使用 shell 的标准输出。

使用 Ctrl-Z 放入后台的进程仍然是终端进程的子进程,一旦关闭终端(会发送另外一个信号 SIGHUP),这些后台的进程也会终止。为了防止这种情况发生,可以使用 nohup (一个用来忽略 SIGHUP 的封装) 来运行程序。针对已经运行的程序,可以使用 disown

$ sleep 1000
^Z
[1]  + 18653 suspended  sleep 1000

$ nohup sleep 2000 &
[2] 18745
appending output to nohup.out

$ jobs
[1]  + suspended  sleep 1000
[2]  - running    nohup sleep 2000

$ bg %1
[1]  - 18653 continued  sleep 1000

$ jobs
[1]  - running    sleep 1000
[2]  + running    nohup sleep 2000

$ kill -STOP %1
[1]  + 18653 suspended (signal)  sleep 1000

$ jobs
[1]  + suspended (signal)  sleep 1000
[2]  - running    nohup sleep 2000

$ kill -SIGHUP %1
[1]  + 18653 hangup     sleep 1000

$ jobs
[2]  + running    nohup sleep 2000

$ kill -SIGHUP %2

$ jobs
[2]  + running    nohup sleep 2000

$ kill %2
[2]  + 18745 terminated  nohup sleep 2000

$ jobs 

终端多路复用

终端多路复用使我们可以分离当前终端会话并在将来重新连接。这让您操作远端设备时的工作流大大改善,避免了 nohup 和其他类似技巧的使用。

现在最流行的终端多路器是 tmux

  • 会话 - 每个会话都是一个独立的工作区,其中包含一个或多个窗口

    • tmux 开始一个新的会话
    • tmux new -s NAME 以指定名称开始一个新的会话
    • tmux ls 列出当前所有会话
    • tmux 中输入 <C-b> d(detach),将当前会话分离
    • tmux a(attach)重新连接最后一个会话。您也可以通过 -t 来指定具体的会话
  • 窗口 - 相当于编辑器或是浏览器中的标签页,从视觉上将一个会话分割为多个部分

    • <C-b> c 创建一个新的窗口,使用 <C-d>关闭
    • <C-b> N 跳转到第 N 个窗口,注意每个窗口都是有编号的
    • <C-b> p(previous)切换到前一个窗口
    • <C-b> n(next)切换到下一个窗口
    • <C-b> , 重命名当前窗口
    • <C-b> w 列出当前所有窗口
  • 面板 - 像 vim 中的分屏一样,面板使我们可以在一个屏幕里显示多个 shell

    • <C-b> " 水平分割
    • <C-b> % 垂直分割
    • <C-b> <方向> 切换到指定方向的面板,<方向> 指的是键盘上的方向键
    • <C-b> z(zoom)切换当前面板的缩放
    • <C-b> [ 开始往回卷动屏幕。您可以按下空格键来开始选择,回车键复制选中的部分
    • <C-b> <空格> 在不同的面板排布间切换

扩展阅读: 这里 是一份 tmux 快速入门教程, 而这一篇 文章则更加详细,它包含了 screen 命令。您也许想要掌握 screen 命令,因为在大多数 UNIX 系统中都默认安装有该程序。

别名

# colorls
source $(dirname $(gem which colorls))/tab_complete.sh
alias ls=colorls
alias l="ls -lh"
alias ll="ls -lAh"
alias la="ls -lah"

alias hz="history | fzf"
alias mv="mv -i"
alias cp="cp -i"
alias mkdir="mkdir -p"

# To ignore an alias run it prepended with 
\ls
# Or disable an alias altogether with unalias
unalias la

# To get an alias definition just call it with alias
alias l
# Will print l='ls -lh'

配置文件(Dotfiles)

管理配置文件的一个方法是,把它们集中放在一个文件夹中,例如 ~/.dotfiles/,并使用版本控制系统进行管理,然后通过脚本将其 符号链接 到需要的地方。这么做有如下好处:

  • 安装简单: 如果您登录了一台新的设备,在这台设备上应用您的配置只需要几分钟的时间;
  • 可以执行: 您的工具在任何地方都以相同的配置工作
  • 同步: 在一处更新配置文件,可以同步到其他所有地方
  • 变更追踪: 您可能要在整个程序员生涯中持续维护这些配置文件,而对于长期项目而言,版本历史是非常重要的

一些技巧:

if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi

# 使用和 shell 相关的配置时先检查当前 shell 类型
if [[ "$SHELL" == "zsh" ]]; then {do_something}; fi

# 您也可以针对特定的设备进行配置
if [[ "$(hostname)" == "myServer" ]]; then {do_something}; fi

# Test if ~/.aliases exists and source it
if [ -f ~/.aliases ]; then
    source ~/.aliases
fi

远端设备

SSH (Secure Shell)

# 连接设备
ssh foo@bar.mit.edu 
ssh foobar@192.168.1.42
# 如果存在配置文件,可以简写
ssh bar

# 执行命令
# 在本地查询远端 ls 的输出
ssh foobar@server ls | grep PATTERN
# 在远端对本地 ls 输出的结果进行查询
ls | ssh foobar@server grep PATTERN

SSH 密钥

基于密钥的验证机制使用了密码学中的公钥,我们只需要向服务器证明客户端持有对应的私钥,而不需要公开其私钥。这样您就可以避免每次登录都输入密码的麻烦了秘密就可以登录。

ssh-keygen -t ed25519 -C "_your_email@example.com_"
# If you are using a legacy system that doesn't support the Ed25519 algorithm, use:
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

生成的 id_rsaid_rsa.pub 两个文件(或者 id_ed25519id_ed25519.pub),分别为的私钥公钥。私钥等效于你的密码,所以一定要好好保存它。要检查您是否持有某个密钥对的密码并验证它,您可以运行 ssh-keygen -y -f /path/to/key

ssh 会查询 .ssh/authorized_keys 来确认那些用户可以被允许登录。您可以通过下面的命令将一个公钥拷贝到这里:

cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys' 

如果支持 ssh-copy-id 的话,可以使用下面这种更简单的解决方案:

ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote

通过 SSH 复制文件

使用 ssh 复制文件有很多方法:

  • ssh+tee, 最简单的方法是执行 ssh 命令,然后通过这样的方法利用标准输入实现 cat localfile | ssh remote_server tee serverfile。回忆一下,tee 命令会将标准输出写入到一个文件;
  • scp :当需要拷贝大量的文件或目录时,使用scp 命令则更加方便,因为它可以方便的遍历相关路径。语法如下:scp path/to/local_file remote_host:path/to/remote_file
  • rsyncscp 进行来改进,它可以检测本地和远端的文件以防止重复拷贝。它还可以提供一些诸如符号连接、权限管理等精心打磨的功能。甚至还可以基于 --partial标记实现断点续传。rsync 的语法和scp类似。

端口转发

本地端口转发Local Port Forwarding

远程端口转发Remote Port Forwarding

常见的情景是使用本地端口转发,即远端设备上的服务监听一个端口,而您希望在本地设备上的一个端口建立连接并转发到远程端口上。例如,我们在远端服务器上运行 Jupyter notebook 并监听 8888 端口。 然后,建立从本地端口 9999 的转发,使用 ssh -L 9999:localhost:8888 foobar@remote_server 。这样只需要访问本地的 localhost:9999 即可。

SSH 配置

使用 ~/.ssh/config 文件来创建别名,类似 scprsyncmosh的这些命令都可以读取这个配置并将设置转换为对应的命令行选项。

Host vm
    User foobar
    HostName 172.16.174.141
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888

# 在配置文件中也可以使用通配符
Host *.mit.edu
    User foobaz 

服务器侧的配置通常放在 /etc/ssh/sshd_config。您可以在这里配置免密认证、修改 shh 端口、开启 X11 转发等等。也可以为每个用户单独指定配置。

杂项

连接远程服务器的一个常见痛点是遇到由关机、休眠或网络环境变化导致的掉线。如果连接的延迟很高也很让人讨厌。Mosh(即 mobile shell )对 ssh 进行了改进,它允许连接漫游、间歇连接及智能本地回显。

有时将一个远端文件夹挂载到本地会比较方便, sshfs 可以将远端服务器上的一个文件夹挂载到本地,然后您就可以使用本地的编辑器了。

Shell & 框架

常见的 Shell:

常见的 Shell 框架:

终端模拟器

一些经典的模拟器:

一些新兴的模拟器(通常具有更好的性能,例如下面两个具有 GPU 加速):

课后练习

任务控制

习题 1

$ sleep 1000
^Z
[1]  + 689 suspended  sleep 1000

$ sleep 2000                                                                   
^Z
[2]  + 697 suspended  sleep 2000

$ jobs
[1]  - suspended  sleep 1000
[2]  + suspended  sleep 2000

$ bg %1
[1]  - 689 continued  sleep 1000

$ jobs
[1]  - running    sleep 1000
[2]  + suspended  sleep 2000

$ pgrep -af "sleep 1"
689 sleep 1000

$ pkill -f "sleep 1"
[1]  - 689 terminated  sleep 1000

$ jobs
[2]  + suspended  sleep 2000

$ pkill -f "sleep 2"

$ jobs
[2]  + suspended  sleep 2000

$ pkill -9 -f "sleep 2"
[2]  + 697 killed     sleep 2000

$ jobs

参见 man kill,默认发送的信号是 TERM。-9 等价于 -SIGKILL 或者 -KILL

习题 2

$ sleep 10 &
[1] 1121

$ pgrep sleep | wait ; ls
[1]  + 1121 done       sleep 10

   Nothing to show here
$ pidwait() {
    wait $1
    echo "done"
    eval $2
}

$ sleep 10 &
[1] 1420

$ pidwait 1420 "ls"
[1]  + 1420 done       sleep 10
done

   Nothing to show here
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 8 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-08-03
个人主页被 1.7k 人浏览