操作系统总结


上机代码

进程调度

最短作业优先实现

用一个优先队列就可以达到 O ( n log ⁡ n ) \mathcal{O}(n \log n) O(nlogn)的时间复杂度。

#include 
#include 
#include 
#include 
#include 
using namespace std;

using ll = long long;
using ld = long double;

struct process {
    int arr, dur, i, start, end, cyc;
    bool operator< (const process& other) const {
        if (dur == other.dur) return arr > other.arr;
        return dur > other.dur;
    }
};

vector p;
priority_queue que;

int main() {
    if (fopen("test.in", "r") != nullptr) freopen("test.in", "r", stdin);
    int n, arr, dur;
    scanf("%d", &n);
    p.assign(n, {0, 0, -1, 0, 0, 0});
    for (int i = 0; i < n; ++i) {
        scanf("%d%d", &arr, &dur);
        p[i] = {arr, dur, -1, -1, -1, -1}; 
    }
    sort(p.begin(), p.end(), [](const process& a, const process& b) {
        return a.arr < b.arr;
    });
    for (int i = 0; i < n; ++i) p[i].i = i;
    int cur = 0;
    for (int i = 0; i < n || !que.empty(); ) {
        if (i < n) cur = max(p[i].arr, cur);
        while (i < n && p[i].arr <= cur) que.push(p[i]), ++i;
        if (!que.empty()) {
            process now = que.top();
            que.pop();
            p[now.i].start = cur;
            cur += now.dur;
            p[now.i].end = cur;
            p[now.i].cyc = cur - now.arr;
        }
    }
    sort(p.begin(), p.end(), [](const process& a, const process& b) {
        return a.start < b.start;
    });
    ll cyc = 0;
    ld weight_cyc = 0;
    printf("pid\t\tarrive time\tstart time\tend time\trun time\tcycle time\tweighted cycle time\n");
    for (int i = 0; i < n; ++i) {
        printf("p[%d]:\t\t%d\t\t%d\t\t%d\t\t%d\t\t%d\t\t%.6Lf\n", p[i].i, p[i].arr, p[i].start, p[i].end, p[i].dur, p[i].cyc, static_cast(p[i].cyc) / p[i].dur);
        cyc += p[i].cyc;
        weight_cyc += static_cast(p[i].cyc) / p[i].dur;
    }
    printf("avg cyc time: %.15Lf\navg weighted cyc time: %.15Lf\n", static_cast(cyc) / n, weight_cyc / n);
    return 0;
}

(1.in):

16
0 1
1 35
2 10
3 5
6 9
7 21
9 35
11 23
12 42
13 1
14 7
20 5
23 3
24 22
25 31
26 1

输出如下:

c:\School\OS\20190415
λ SJF.exe
pid             arrive time     start time      end time        run time        cycle time      weighted cycle time
p[0]:           0               0               1               1               1               1.000000
p[1]:           1               1               36              35              35              1.000000
p[9]:           13              36              37              1               24              24.000000
p[15]:          26              37              38              1               12              12.000000
p[12]:          23              38              41              3               18              6.000000
p[3]:           3               41              46              5               43              8.600000
p[11]:          20              46              51              5               31              6.200000
p[10]:          14              51              58              7               44              6.285714
p[4]:           6               58              67              9               61              6.777778
p[2]:           2               67              77              10              75              7.500000
p[5]:           7               77              98              21              91              4.333333
p[13]:          24              98              120             22              96              4.363636
p[7]:           11              120             143             23              132             5.739130
p[14]:          25              143             174             31              149             4.806452
p[6]:           9               174             209             35              200             5.714286
p[8]:           12              209             251             42              239             5.690476
avg cyc time: 78.187500000000000
avg weighted cyc time: 6.875675357056844

死锁避免算法

银行家算法

要求的是什么

一个序列,表示进程按照该序列进行处理,就不会发生死锁。

思路

由于有很多种资源,所以资源本身用一个向量表示。我们不妨假设向量支持+=<=等运算符。

用一个have向量保存目前每一种资源剩余多少可用。

每次寻找中,贪心的寻找这样一个进程,使得:

  • 它需要的资源的数量,每一种都不多于对应种类可用资源的数量。

我们用alloc[i]表示第i个进程目前已经分配到的资源,tot[i]表示第i个进程一共需要多少资源。显然,进程i还需要tot[i] - alloc[i]的资源。

我们再用一个数组fin[i]表示当前进程是否已经被加入结果序列中。

于是,我们很容易写出如下代码:

    for (int i = 0; i < n; ++i) {
        if (!fin[i] && tot[i] - alloc[i] <= have) {
            have += alloc[i];
            res.push_back(i);
            return fin[i] = true;
        }
    }

我们一直寻找这样的进程,直到这样的进程不再存在。

此时有两种情况:

  1. 所有进程都被加入了输出序列
  2. 有一个或多个进程不能获得其需要的资源。

不论是哪一种,在这种情况产生的时候就应该停止继续寻找进程。我们可以通过这种方法实现:

bool simulate() {
    for (int i = 0; i < n; ++i) {
        if (!fin[i] && tot[i] - alloc[i] <= have) {
            have += alloc[i];
            res.push_back(i);
            return fin[i] = true;
        }
    }
    return false;
}

int main() {
    // ...
    while (simulate());
    return 0;
}

显然,在这种实现下,银行家算法的时间复杂度为 O ( n 2 ) \mathcal{O}(n^2) O(n2)。更快的实现暂时还没有余力去思考。

代码

完全体中包含了一个自定义的向量结构体,用于支持:

  • operator=()
  • operator<=()
  • operator+=()

这类的操作,使得核心算法部分的可读性有一定程度的提高(大概)

其中的set(int n)方法是设定该向量的长度为n

剩余的读入顺序应该很容易就能从main中的命名读懂。

#include 
#include 
#include 
#include 
using namespace std;

/*
 * n: the number of process
 * m: the number of resources
 */
int n, m;

struct vec {
    vector v;
    int n;

    explicit vec(int new_n = 0) {
        if (new_n) set(new_n);
    }

    virtual ~vec() {
        v.clear();
    }

    vec(const vec& other) {
        v = other.v;
    }

    void set(int new_n) {
        v.clear();
        n = new_n;
        v.assign(n, 0);
    }

    vec& operator=(const vec& other) {
        v = other.v;
        return *this;
    }

    vec operator-(const vec& other) {
        vec res(*this);
        assert(res.v.size() == other.v.size());
        for (int i = 0; i < res.v.size(); ++i) res.v[i] -= other.v[i];
        return res;
    }
    
    bool operator<=(const vec& other) const {
        if (v.size() != other.v.size()) return false;
        for (int i = 0; i < v.size(); ++i)
            if (v[i] > other.v[i])
                return false;
        return true;
    }

    void operator+=(const vec& other) {
        assert(v.size() == other.v.size());
        for (int i = 0; i < v.size(); ++i) v[i] += other.v[i];
    }

    int& operator[](int i) {
        return v[i];
    }

    friend istream& operator>>(istream& is, vec& v) {
        for (int i = 0; i < v.n; ++i) is >> v[i];
        return is;
    }

    friend ostream& operator<<(ostream& os, const vec& v) {
        os << '[';
        for (int i = 0; i < v.n; ++i) {
            if (i) os << ", ";
            os << v.v[i];
        }
        os << ']';
        return os;
    }
} have, fin, * tot, * alloc;

vector res;

bool simulate() {
    for (int i = 0; i < n; ++i) {
        if (!fin[i] && tot[i] - alloc[i] <= have) {
            have += alloc[i];
            res.push_back(i);
            return fin[i] = true;
        }
    }
    return false;
}

int main() {
    ios::sync_with_stdio(false);
    if (fopen("1.in", "r") != nullptr) freopen("1.in", "r", stdin);

    while (cin >> n >> m) {
        res.clear();
        have.set(m);
        cin >> have;
        fin.set(n);
        alloc = new vec[n];
        for (int i = 0; i < n; ++i) {
            alloc[i].set(m);
            cin >> alloc[i];
        }
        tot = new vec[n];
        for (int i = 0; i < n; ++i) {
            tot[i].set(m);
            cin >> tot[i];
        }

        while (simulate());

        if (res.size() == n) {
            cout << "YES" << endl;
            cout << res[0];
            for (int i = 1; i < res.size(); ++i) cout << " -> " << res[i];
            cout << endl;
        } else {
            cout << "NO" << endl;
        }
        delete [] alloc;
        delete [] tot;
    }
    return 0;
}

输入文件(1.in):

5 3
3 3 2

0 1 0
2 0 0
3 0 2
2 1 1
0 0 2

7 5 3
3 2 2
9 0 2
2 2 2
4 3 3

5 4
1 2 2 2

2 0 1 1
0 1 2 1
4 0 0 3
0 2 1 0
1 0 3 0

3 2 1 4
0 2 5 2
5 1 0 5
1 5 3 0
3 0 3 3

输出:

YES
1 -> 3 -> 0 -> 2 -> 4
YES
2 -> 0 -> 1 -> 3 -> 4

页置换算法

最佳置换算法

反正也是不可能在现实中实现的算法,所以就放飞自我了(这人

大致思想就是,首先对数组每一个元素求一下它下一次出现的位置。这可以用unordered_map O ( n ) \mathcal{O}(n) O(n)的时间复杂度完成。

之后就是模拟算法过程。对于当前元素,

  • 如果内存里已经有这个元素,跳过
  • 否则如果内存有空闲就直接使用
  • 否则扫一遍数组看看哪个元素的下一次出现的位置最远,替换它

代码:
时间复杂度 O ( n ⋅ m ) \mathcal{O}(n \cdot m) O(nm),其中 m m m是系统给进程分配的内存的物理块数。

P.S: 有很多看起来很多余的函数,纯粹是因为还要实现FIFO, LRU之类的算法,为了能够重复使用才单独声明成函数的。

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

const int N = 1e5 + 5;
int PAGE_SIZE, MEM_SIZE;
int a[N];
int nxt[N];
int pre[N];
int n, cnt;

struct custom_hash {
    static uint64_t splitmix64(uint64_t x) {
        // http://xorshift.di.unimi.it/splitmix64.c
        x += 0x9e3779b97f4a7c15;
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
        x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
        return x ^ (x >> 31);
    }

    size_t operator()(uint64_t x) const {
        static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
        return splitmix64(x + FIXED_RANDOM);
    }
};

using safe_map = unordered_map<int, int, custom_hash>;
safe_map rec;

bool in(int* mem, int pos) {
    for (int i = 0; i < MEM_SIZE; ++i)
        if (a[mem[i]] / PAGE_SIZE == a[pos] / PAGE_SIZE) {
            mem[i] = pos;
            return true;
        }
    return false;
}

void has(int val) {
    cout << val << " already in memory" << endl;
}

void empty(int pos, int val) {
    cout << "Page " << pos << " is empty, " << val << " inserted" << endl;
    ++cnt;
}

void replace(int pos, int val) {
    cout << "Page " << pos << " is replaced by " << val << endl;
    ++cnt;
}

void debug(int* mem) {
    // for (int i = 0; i < 3; ++i) cout << mem[i] << ' ';
    // cout << endl;
    for (int i = 0; i < MEM_SIZE; ++i) cout << (~mem[i] ? a[mem[i]] / PAGE_SIZE : -1) << ' ';
    cout << endl;
    // for (int i = 0; i < 3; ++i) cout << (~mem[i] ? nxt[mem[i]] : -1) << ' ';
    // cout << endl;
}

void print_stat() {
    cout << "Page fault cnt: " << cnt << ", rate: " << setprecision(10)
         << static_cast<double>(cnt) / n << endl;
}

void opt() {
    rec.clear();
    cnt = 0;
    for (int i = n - 1; ~i; --i) {
        nxt[i] = rec.count(a[i] / PAGE_SIZE) ? rec[a[i] / PAGE_SIZE] : -1;
        rec[a[i] / PAGE_SIZE] = i;
    }
    for (int i = 0; i < n; ++i) cout << nxt[i] << ' ';
    cout << endl;
    int* mem = new int[MEM_SIZE];
    fill(mem, mem + MEM_SIZE, -1);
    for (int i = 0; i < n; ++i) {
        if (in(mem, i)) {
            has(a[i]);
        } else {
            int j = 0;
            for (; j < MEM_SIZE && ~mem[j]; ++j);
            if (j == MEM_SIZE) --j;
            if (~mem[j]) {
                int min_nxt = 0;
                if (~nxt[mem[0]])
                    for (int k = 1; k < MEM_SIZE; ++k)
                        if (nxt[mem[k]] > nxt[mem[min_nxt]] || nxt[mem[k]] == -1) {
                            min_nxt = k;
                            if (nxt[mem[k]] == -1) break;
                        }
                replace(min_nxt, a[i]);
                mem[min_nxt] = i;
            } else {
                empty(j, a[i]);
                mem[j] = i;
            }
        }
        debug(mem);
    }
    print_stat();
    delete [] mem;
}

int main() { 
    if (fopen("1.in", "r") != nullptr) freopen("1.in", "r", stdin);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    while(cin >> n >> PAGE_SIZE >> MEM_SIZE) {
        for (int i = 0; i < n; ++i) cin >> a[i];
        opt();
        cout << endl;
    }
    return 0;
}

跑出来大概长这样:

(1.in)

12 100 3
202 313 252 111 546 217 444 544 365 223 398 111
12 1 3
2 3 2 1 5 2 4 5 3 2 5 2
20 1 3
1 2 3 4 2 1 5 6 2 1 2 3 7 6 3 2 1 2 3 6

(输出)

c:\School\OS\20190527  
λ PageFault.exe
2 8 5 11 7 9 -1 -1 10 -1 -1 -1
Page 0 is empty, 202 inserted
2 -1 -1
Page 1 is empty, 313 inserted
2 3 -1
252 already in memory
2 3 -1
Page 2 is empty, 111 inserted
2 3 1
Page 2 is replaced by 546
2 3 5
217 already in memory
2 3 5
Page 0 is replaced by 444
4 3 5
544 already in memory
4 3 5
365 already in memory
4 3 5
Page 0 is replaced by 223
2 3 5
398 already in memory
2 3 5
Page 0 is replaced by 111
1 3 5
Page fault cnt: 7, rate: 0.5833333333

2 8 5 -1 7 9 -1 10 -1 11 -1 -1
Page 0 is empty, 2 inserted
2 -1 -1
Page 1 is empty, 3 inserted
2 3 -1
2 already in memory
2 3 -1
Page 2 is empty, 1 inserted
2 3 1
Page 2 is replaced by 5
2 3 5
2 already in memory
2 3 5
Page 0 is replaced by 4
4 3 5
5 already in memory
4 3 5
3 already in memory
4 3 5
Page 0 is replaced by 2
2 3 5
5 already in memory
2 3 5
2 already in memory
2 3 5
Page fault cnt: 6, rate: 0.5

5 4 11 -1 8 9 -1 13 10 16 15 14 -1 19 18 17 -1 -1 -1 -1
Page 0 is empty, 1 inserted
1 -1 -1
Page 1 is empty, 2 inserted
1 2 -1
Page 2 is empty, 3 inserted
1 2 3
Page 2 is replaced by 4
1 2 4
2 already in memory
1 2 4
1 already in memory
1 2 4
Page 2 is replaced by 5
1 2 5
Page 2 is replaced by 6
1 2 6
2 already in memory
1 2 6
1 already in memory
1 2 6
2 already in memory
1 2 6
Page 0 is replaced by 3
3 2 6
Page 1 is replaced by 7
3 7 6
6 already in memory
3 7 6
3 already in memory
3 7 6
Page 1 is replaced by 2
3 2 6
Page 2 is replaced by 1
3 2 1
2 already in memory
3 2 1
3 already in memory
3 2 1
Page 0 is replaced by 6
6 2 1
Page fault cnt: 11, rate: 0.55

磁盘调度算法

SSTF算法

首先简单讲一下SSTF算法:

  • 维护一下当前磁头在哪个磁道。
  • 每次选择距离当前磁道最近的请求,将磁头移动到那个位置。

一开始仔细思考了一下,用普通线段树的话大概只能做到 O ( n 2 log ⁡ n ) \mathcal{O}(n^2 \log n) O(n2logn)的复杂度,还不如对于每个元素扫一遍数组的 O ( n 2 ) \mathcal{O}(n^2) O(n2)来的方便…

后来突然一想…这不是排个序后,用一个双出队列就能 O ( n log ⁡ n + n ) \mathcal{O}(n\log n + n) O(nlogn+n)完成吗???

不过这么写的话就过于ACM了,因为根本没考虑新来的进程: 每个新进程要花 O ( n ) \mathcal{O}(n) O(n)的时间才能被插入双出队列,最后还是接近 O ( n 2 ) \mathcal{O}(n^2) O(n2)才能完成。所以这个算法可以说只有离线执行的时候能够到达理想的时间复杂度。

不过总归还是比铁定只能 O ( n 2 ) \mathcal{O}(n^2) O(n2)要强一些,于是我是按照 O ( n log ⁡ n + n ) \mathcal{O}(n \log n + n) O(nlogn+n)的方法实现的:

  • 首先对当前请求序列进行排序。
  • 利用当前请求序列创建一个双出队列。
  • 找出比当前磁头位置小的最大请求节点,作为当前节点。

之后,直到双出队列为空之前,每次做如下比较:

  • 看看当前节点的前一个节点和下一个节点,哪个到当前磁道位置比较近。
  • 将磁道位置更新为较近的值,并且更新当前节点的指针。
  • 将原先的节点删掉。

这样我们就能 O ( n ) \mathcal{O}(n) O(n)的完成模拟。

离线算法代码

模拟部分写的又臭又长,简直堪比上学期上机手写二叉树的两种删除操作。

#include 
#include 
#include 
#include 
using namespace std;

const int MAXN = 1e5 + 5;
int a[MAXN];

struct node {
    int pos;
    node* prev;
    node* next;

    explicit node() {
        prev = next = nullptr;
        pos = -1;
    }

    node(int pos, node* prev, node* next): pos(pos), prev(prev), next(next) {}
};

struct deque {
    struct node* head;
    struct node* tail;
    uint64_t __len;

    explicit deque() {
        head = new node();
        tail = new node();
        head->next = tail;
        tail->prev = head;
        __len = 0;
    }

    virtual ~deque() {
        delete head;
        delete tail;
    }

    node* append(int pos) {
        node* cur = new node(pos, tail->prev, tail);
        tail->prev = tail->prev->next = cur;
        ++__len;
        return cur;
    }

    void remove(node* tar) {
        node* tar_prev = tar->prev;
        node* tar_next = tar->next;
        tar_prev->next = tar_next;
        tar_next->prev = tar_prev;
        delete tar;
        --__len;
    }

    uint64_t size() const {
        return __len;
    }

    bool empty() const {
        return __len == 0;
    }

    friend ostream& operator<<(ostream& os, deque& que) {
        os << '[';
        for (node* cur = que.head->next; cur != que.tail; cur = cur->next) {
            if (cur != que.head->next) {
                os << ", ";
            }
            os << cur->pos;
        }
        os << ']';
        return os;
    }
};

int dist(int a, int b) {
    int ans = a - b;
    return (ans < 0 ? -ans : ans);
}

int main() {
    ios::sync_with_stdio(false);
    if (fopen("1.in", "r") != nullptr) freopen("1.in", "r", stdin);
    int n, cur;
    cin >> n >> cur;
    for (int i = 0; i < n; ++i) cin >> a[i];
    sort(a, a + n);
    node* ptr = nullptr, * new_ptr;
    deque que;
    for (int i = 0; i < n; ++i) {
        if (a[i] <= cur) {
            ptr = que.append(a[i]);
        } else {
            que.append(a[i]);
        }
    }
    int ans = 0;
    cout << cur;
    if (ptr == nullptr) ptr = que.head->next;
    else {
        if (ptr->next == que.tail) new_ptr = ptr->prev;
        else {
            if (dist(cur, ptr->pos) < dist(cur, ptr->next->pos)) new_ptr = ptr;
            else new_ptr = ptr->next;
        }
        ptr = new_ptr;
    }
    ans += dist(ptr->pos, cur);
    cur = ptr->pos;
    while (que.size() > 1) {
        cout << " -> " << ptr->pos;
        if (ptr->next == que.tail) new_ptr = ptr->prev;
        else if (ptr->prev == que.head ) new_ptr = ptr->next;
        else {
            if (dist(cur, ptr->prev->pos) < dist(cur, ptr->next->pos)) new_ptr = ptr->prev;
            else new_ptr = ptr->next;
        }
        que.remove(ptr);
        ptr = new_ptr;
        ans += dist(ptr->pos, cur);
        cur = ptr->pos;
    }
    cout << endl << "Total distance: " << ans << endl;
    return 0;
}

输入(1.in):

8 53
98 183 37 122 14 124 65 67

输出:

53 -> 65 -> 67 -> 37 -> 14 -> 98 -> 122 -> 124 -> 183

在线算法代码

既然上面的subtitle写了离线,说明肯定有在线算法。

这个算法是受到组里紫名dalao的启发而产生的,核心是通过平衡二叉树实现 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn)级别的复杂度。

本来的写法是将上面的deque变成set,之后的思路都是一致的: 每次寻找中缀遍历的前驱和后继,移动到近的那个节点。这很好写,因为set::iterator的自增和自减等同于寻找中缀遍历的前驱和后继。不过它的复杂度我没有搜到相关资料,只知道使用了LegacyBidirectionalIterator

所以,最后还是抄袭借鉴了dalao的代码,因为写起来确实方便,而且鲁棒性很不错。

输入

第一行两个整数,分别表示初始状态下有 n n n个请求,以及当前磁道数 c u r cur cur

第二行 n n n个整数,代表初始状态下每个请求。

接下来,会有很多次操作,你的程序应该处理到EOF

操作有两种类型:

  • ADD X,也就是增加一个访问磁道X的请求。
  • GET,求出下一个应当被访问的磁道数,交给操作系统去读取。注意,求出这个磁道数后,该请求就被完成了,也就是说它会从请求列表中被移除。

输出

人类能看得懂就行,反正不是OJ。(ACMer激怒)

思路

抄袭借鉴了紫名大佬的代码。不得不说虽然复杂度从 O ( q ) \mathcal{O}(q) O(q)变成了 O ( q log ⁡ q ) \mathcal{O}(q \log q) O(qlogq),但是写起来方便多了…

可能就是自己太菜了连这种程度的代码都写不对…

代码

#include 
#include 
#include 
#include 
#include 
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    if (fopen("sstf_ol.in", "r") != nullptr) freopen("sstf_ol.in", "r", stdin);
    string op;
    int n, temp, cur;
    set s;
    int ans = 0;

    cin >> n >> cur;
    cout << cur;
    for (int i = 0; i < n; ++i) {
        cin >> temp;
        s.insert(temp);
    }

    while (cin >> op) {
        if (op == "ADD") {
            cin >> temp;
            s.insert(temp);
        } else {
            assert(op == "GET");
            temp = cur;
            auto low = s.lower_bound(cur), up = s.upper_bound(cur);
            if (low != s.begin()) --low;
            if (up == s.end()) cur = *up;
            else if (cur - *low < *up - cur) cur = *low;
            else cur = *up;
            s.erase(cur);
            ans += abs(cur - temp);
            cout << " -> " << cur;
        }
    }
    cout << endl << "Total distance: " << ans << endl;
    return 0;
}

样例1

8 53
98 183 37 122 14 124 65 67
GET
GET
GET
GET
GET
GET
GET
53 -> 65 -> 67 -> 37 -> 14 -> 98 -> 122 -> 124
Total distance: 177

样例2

8 53
98 183 37 122 14 124 65 67
GET
GET
GET
ADD 34
GET
GET
ADD 57
ADD 97
GET
GET
GET
GET
GET
GET
ADD 1
GET
53 -> 67 -> 37 -> 34 -> 14 -> 57 -> 97 -> 98 -> 122 -> 124 -> 183 -> 1
Total distance: 418

样例3

8 99
1 2 3 4 5 6 7 8
GET
GET
GET
GET
GET
GET
GET
ADD 99
GET
GET
99 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 99
Total distance: 196

心得

这个标题搞得像是中小学时强行凑学习心得的感觉,不过这里其实主要讲的还是对于一些关键概念的心得和理解。

信号量

马上就要期末了,这几天的复习也可以说是非常的紧张。所以操作系统相关的博客也算是比较高产了。

这些心得是我洗澡时候想出来的,考虑到自己的健忘性,所以在这里整理下来。

信号量能够做什么

首先,毋庸置疑的第一点:

互斥

当信号量的上界是1的时候,那么说明它的取值无非是0或者1。

这种时候它就可以拿来当作一个bool flag来使用,表示互斥。

更加形象的来说,应该当作一个令牌来处理,也就是说对于每个进程,如果它恰好得到了这个令牌,它就有执行P(mutex)这条语句的权利,并且进入临界区。

一个特征

基本上出现这种结构就是互斥:

P(mutex);
// critical area
V(mutex);

它能做的第二点就是喜闻乐见并且经常初见杀的:

同步

个人感觉同步这个词过于抽象了,也许作为步骤的条件这样来理解比较好。

对于步骤 p i p_i pi来说,如果它必须要求 p i − 1 p_{i - 1} pi1已经完成,那么可以这样书写:

P(p[i - 1]);
// do something
V(p[i]);

它在运行结束的时候会产生 p i p_i pi这一信号量,表明步骤 p i p_i pi已完成。

这样,我们就可以将一个条件放在两个线程中的不同位置,来构成较复杂的同步问题。

一个已经被用烂掉的例子

考虑公交车中司机与售票员的同步:

司机:
  1. 等售票员关门
  2. 开车 & 停车
  3. 开门

售票员:
  1. 等司机停车
  2. 售票
  3. 关门

~~虽然不知道为什么是售票员控制关门,~~总之先来看一下次序:

对于司机而言,要等售票员关门后再开车。也就是说,应当使用P(关门),也就是一直询问关门这个信号是否为 1 1 1(虽然其实实际上并不是忙等,但是这么思考好像更加容易理解)。那么既然有了P(关门),就需要V(关门)

于是我们把这两个信号量放进去:

司机:
  1. P(关门)
  2. 开车 & 停车
  3. 开门

售票员:
  1. 等司机停车
  2. 售票
  3. V(关门)

同理,等司机停车这件事可以用P(停车)来表示。

于是依葫芦画瓢我们可以把整个过程改成:

司机:
  1. P(关门)
  2. 开车 & 停车
  3. V(停车)
  4. 开门

售票员:
  1. P(停车)
  2. 售票
  3. V(关门)

这样我们就用两个信号量说明了两个无限循环的步骤: 关门->停车->关门->停车->...

一个同时使用了互斥和同步的例子

从PPT上看到的理发师问题:

理发店理有一位理发师、一把理发椅和n把供等候理发的顾客坐的椅子。

如果没有顾客,理发师便在理发椅上睡觉。

一个顾客到来时,它必须叫醒理发师。

如果理发师正在理发时又有顾客来到,则如果有空椅子可坐,就坐下来等待,否则就离开。

不难看出,我们要先叫醒理发师才能够理发。所以,步骤0是叫醒理发师,步骤1是理发:

顾客:
 1. 叫醒理发师
 2. 理发
 
理发师:
 1. 等顾客叫醒他
 2. 理发

于是我们可以这样实现同步:

顾客:
  1. V(顾客)
  2. 理发
  
理发师:
  1. P(顾客)
  2. 理发

考虑到还有缓冲区,所以还需要增加一些步骤:

顾客:
  1. 排队: 如果没有空位就走人
  2. V(顾客)
  3. 理发
  
理发师:
  1. P(顾客)
  2. 队列减少一个人
  3. 理发

因为给队列增加一个人这件事情必须是原子的(因为涉及到对人数cnt的修改),所以我们需要给顾客排队这件事加上互斥:

顾客:
  P(mutex);
  if (cnt < n) {
    ++cnt;
    V(mutex);
  	V(顾客);
    理发
  } else {
  	V(mutex);
  }

理发师:
  1. P(顾客)
  2. 队列减少一个人
  3. 理发

不论是cnt < n还是cnt >= n,都会执行等价于释放令牌V(mutex)

思路已经大致撸明白了,之后的步骤就省略了~~

你可能感兴趣的:(操作系统总结)