精华内容
下载资源
问答
  • 夜深人静算法(二九)- 数DP

    千次阅读 2021-03-26 07:17:37
    一、前言 二、数 DP 简介 1、数 DP 定义 2、数 DP 引例 3、状态分析 三、数 DP 代码实现 1、状态初始化 2、数初始化 3、记忆化搜索 四、数 DP 进阶 1、非法状态 2、饱和状态 3、组合状态 4、前导状态 ...

    一、前言

      数位 DP 又称 数位动态规划,在 LeetCode 上属于难题,而 ACM 竞赛中属于中等题,甚至可以说是模板题。
      数位 DP 的状态设计千变万化,但万变不离其宗,只要确定这个题是用数位 DP 求解,基本就很容易把状态套出来。当然,对于刚接触动态规划的同学,建议先看下 背包问题、最长单调子序列、最长公共子序列、记忆化搜索 等基础内容,对状态机和状态转移有一个初步的认识,那么,在学习数位 DP 时能够起到事半功倍的效果。

    二、数位 DP 简介

    1、数位 DP 定义

    • 数位 DP 又称 数位动态规划,一般可以从题干就能确定这个题是否可以用 数位 DP 来求解。因为 数位 DP 的题目一般都描述成如下两种问法之一:

    【问法1】给定一个闭区间 [l,r][l, r],求在这个区间中,满足 某些条件 的数的个数。
    【问法2】如果一个数字满足 某些条件,则称之为 XX 数,给定闭区间 [l,r][l, r],求这个区间中 XX 数的个数。

    • 这里的 某些条件 决定了状态转移的决策,这样说或许比较抽象,那么接下来,我们通过一个简单的例题来了解下 数位DP 的一般求解过程。

    2、数位 DP 引例

    【例题1】如果一个数的所有位数加起来是 1010 的倍数, 则称之为 good numbergood \ number,求区间 [l,r](0lr1018)[l, r](0 \le l \le r \le 10^{18})good numbergood \ number 的个数;

    • 对于这个问题,朴素算法就是枚举区间里的每个数,并且判断可行性,时间复杂度为 o((rl)c)o((r-l)c)c=19c=19,肯定是无法接受的。

    1)差分转换

    • 对于区间 [l,r][l, r] 内求满足数量的数,可以利用差分法分解问题;
    • 假设 [0,x][0, x] 内的 good numbergood \ number 数量为 gxg_x,那么区间 [l,r][l, r] 内的数量就是 grgl1g_r - g_{l-1};分别用同样的方法求出 grg_rgl1g_{l-1},再相减即可;
      在这里插入图片描述
      图二-2-1

    2)数位性质

    • 如果一个数字 ii 满足 i<xi < x,那么 ii 从高到低肯定出现某一位,使得这一位上的数值小于 xx 对应位上的数值,并且之前的所有高位都和 xx 上的位相等。
    • 举个例子,当 x=1314x = 1314 时,i=0abci=0abci=12abi=12abi=130ai=130ai=1312i=1312,那么对于 ii 而言,无论后面的字母取什么数字,都是满足 i<xi < x 这个条件的。
    • 如图二-2-2所示:
      在这里插入图片描述
      图二-2-2
    • 如果我们要求 g1314g_{1314} 的值,可以通过枚举高位:当最高位为0,那么问题就转化成 g999g_{999} 的子问题;当最高位为1,问题就转化成 g314g_{314} 的子问题。
    • g314g_{314} 可以继续递归求解,而 g999g_{999} 由于每一位数字范围都是 [0,9][0,9],可以转换成一般的动态规划问题来求解。

    3)前缀状态

    • 这里的前缀状态就对应了之前提到的 某些条件
    • 在这个问题中,前缀状态的描述为:一个数字前缀的各位数字之和对10取余(模)的值。
    • 前缀状态的变化过程如图二-2-3所示:
      在这里插入图片描述
      图二-2-3
    • 在【例题1】中,前缀状态的含义是:对于一个数 520 ,我们不需要记录 520 ,而只需要记录 7;对于 52013,我们不需要记录 52013,而只需要记录 1。这样就把原本海量的状态,变成了最多 10 个状态。

    3、状态分析

    1)状态定义

    • 根据以上的三个信息,我们可以从高位到低位枚举数字 ii 的每一位,逐步把问题转化成小问题求解。
    • 我们可以定义 f(n,st,lim)f(n, st, lim) 表示剩下还需要考虑 nn 位数字,前面已经枚举的高位组成的前缀状态为 stst,且用 limlim 表示当前第 nn 位是否能够取到最大值(对于 bb 进制,最大值就是 b1b-1,比如 10 进制状态下,最大值就是 9) 时的数字个数。我们来仔细解释一下这三维代表的含义:
    • 1)当前枚举的位是 nn 位,nn 大的代表高位,小的代表低位;
    • 2)stst 就是前缀状态,在这个问题中,代表了所有已经枚举的高位(即数字前缀)的各位数字之和对10取余(模)。注意:我们并不关心前缀的每一位数字是什么,而只关心它们加和模10之后的值是什么。
      图二-3-1
    • 3)lim=truelim=true 表示的是已经枚举的高位中已经出现了某一位比给定 xx 对应位小的数,那么后面枚举的每个数最大值就不再受 xx 控制;否则,最大值就应该是 xx 的对应位。举例说明,当十进制下的数 x=1314x = 1314 时,枚举到高位前三位为 “131”,lim=falselim = false, 那么第四位数字的区间取值就是 [0,4][0,4];而枚举到高位前三位为 “130” 时,lim=truelim = true,那么第四位数字的区间取值就是 [0,9][0, 9]。参考 图二-2-2 加深理解。

    2)状态转移

    • 所以,我们根据以上的状态,预处理 xx 的每个数位,表示成十进制如下:
    • x=dndn1...d1x = d_nd_{n-1}...d_1
    • (其中 dnd_n 代表最高位,d1d_1 代表最低位)
    • 得出状态转移方程如下:
    • f(n,st,lim)=k=0maxvf(n1,(st+k)mod  10,lim or (k<maxv))\begin{aligned}& f(n, st, lim) \\ &= \sum_{k=0}^{maxv} f(n-1, (st+k) \mod 10, lim \ or \ (k < maxv))\end{aligned}
    • kk 表示第 nn 位取什么数字,它的范围是 [0,maxv][0, maxv]
    • maxvmaxv 表示第 nn 位能够取到的最大值,它由 limlim 决定,即:
    • maxv={9lim=truednlim=falsemaxv = \begin{cases}9 & lim = true\\d_n & lim = false\end{cases}

    3)初始状态

    • 利用上述的状态转移方程,我们可以进行递归求解,并且当 n=0n=0 的时候为递归出口,由于数字要求满足所有数字位数之和为 1010 的倍数,所以只有 st=0st = 0 的情况为合法状态,换言之,当 n=0n=0 时,有:
    • f(0,x,lim)={1x=000<x9f(0, x, lim) = \begin{cases} 1 & x = 0\\ 0 & 0 \lt x \le 9\end{cases}
    • 而我们需要求的,就是 f(n,0,false)f(n, 0, false)

    4)记忆化

    • 我们发现,如果按照以上的状态转移进行求解,会导致一个问题,就是会有很多重叠子问题,所以需要进行记忆化,比较简单的方法就是用一个三维数组 f[n][st][lim]f[n][st][lim] 来记忆化。
    • 当然,这里有一个技巧,就是 limlim 这个变量只有 truetruefalsefalse 两种取值,并且当它为 falsefalse 时,代表之前枚举的数的高位和所求区间 [0,x][0, x] 右端点中的 xx 的高位保持完全一致,所以当 lim=falselim = false 时,深度优先搜索树的分支最多只有 11 条,所以无须记忆化,每次直接算就可以。如图二-3-2所示的蓝色结点,就是那条唯一分支。
      在这里插入图片描述
      图二-3-2
    • 综上所述,我们只需要用 f[n][st]f[n][st] 表示长度为 nn,且每个数字的范围为 [0,maxv][0, maxv],且前缀状态为 stst 的数字个数(这里 maxvmaxv 和进制有关,如果是 bb 进制,那么 maxv=b1maxv = b - 1)。
    • f(n,st,false)f(n, st, false) 采用普通深搜,f(n,st,true)f(n, st, true) 采用记忆化搜索。

    三、数位 DP 代码实现

    数位 DP 计算过程主要分为以下几步:
      1、状态初始化
      2、数位初始化
      3、记忆化搜索

    1、状态初始化

    • 状态初始化主要是初始化 f[n][st]f[n][st] 数组,将数组中的所有值都初始化为一个小于 0 的数即可,一般用 -1。
    • c++ 代码实现如下:
    const int maxl = 20;
    const int maxstate = 10;
    const int inf = -1;
    #define ll long long
    ll f[maxl][maxstate];
    void init() {
        memset(f, inf, sizeof(f));	
    }
    

    2、数位初始化

    • 数位初始化就是将给定的区间 [0,x][0, x] 的右端点 xx 按照数位分解到数组 d[]d[] 中;
    • 需要注意的是处理边界情况: x<0x \lt 0 以及 x=0x = 0 的情况;
    • c++ 代码实现如下:
    const int base = 10;
    ll g(ll x) {
        if (x < 0) return 0;
        if (x == 0) return 1;
        int d[maxl];
        int n = 0;
        while (x) {
            d[++n] = x % base;
            x /= base;
        }
        return dfs(n, 0, false, d);
    }
    

    3、记忆化搜索

    • 记忆化搜索部分就是数位 DP 的核心实现,先给出代码,再来解释代码的含义:
    ll dfs(int n, stType state, bool lim, int d[]) {
        if(n == 0)
            return isEndStateValid(state) ? 1 : 0; // 1) 
        ll sum = f[n][state];
        if(lim && sum != inf) return sum;          // 2)
        sum = 0;
        int maxv = lim ? base - 1 : d[n];          // 3)
        for(int k = 0; k <= maxv; ++k) {
            stType st = nextState(state, k);
            bool nextlim = (k < maxv) || lim;
            sum += dfs(n-1, st, nextlim, d);       // 4)
        } 
        if(lim) f[n][state] = sum;                 // 5)
        return sum;
    }
    
    • 1)当 n=0n = 0 的时候,即为递归出口,也就是所有的数位都已经枚举完毕,这时候通过 所有数字之和对 10 取余数的值来判断是否是一个题目中要求的数,是则返回 1,否则返回 0;通过isEndStateValid(state)进行判定:
    #define stType int
    bool isEndStateValid(stType state) {
    	return state == 0;
    }
    
    • 2)当 lim = true,表示接下来所有数的取值都不受给定的 xx 限制,这时候如果f[n][state]已经计算过了,则直接返回即可;
    • 3)通过 lim来决定当前第 nn 位数字枚举的上限:当lim = true时,上限为 base-1;否则,为 dnd_n
    • 4)这一步主要做状态转移,封装出一个 nextState(state, k)函数来生成当前位取 kk 时的下一步的状态,对于【例题1】来说,函数实现如下:
    stType nextState(stType st, int digit) {
    	return (st + digit) % 10;
    }
    
    • 5)当 lim = true,进行记忆化操作,和步骤 (2)相照应;
    • 通过【例题1】,我们了解了一下 数位 DP 的大致求解过程,这个题也是最简单的,接下来我们来看下竞赛中一般会遇到什么样的问题。你会发现,除了状态设计和状态转移部分的差别,整体框架代码都是不变的,所以说 数位DP 就是模板题。

    四、数位 DP 进阶

    1、非法状态

    • 所谓非法状态,就是对于某个数字,在它的某个前缀已经能够判定这个数不满足给定题目的条件时,无须继续往下枚举,而这个前缀状态被称为非法状态,我们通过【例题2】来理解下非法状态的实际含义。

    【例题2】对于一个数字,如果出现 4 或者 62 ,则属于不吉利数字,给定 l,r(0<l<r<106)l, r(0 \lt l \lt r < 10^6),求区间 [l,r][l, r] 中非不吉利数字的个数。

    • 对于任意一个在区间 [l,r][l, r] 内的数,它的某个前缀的最后一位数字只要是 “4” 或者 最后两位数字只要是 “62” 就一定不是我们要求的数,换言之,只要一个数的前缀以 “4” 或者 “62” 结尾,那么就一定是非法前缀状态,简称非法状态。
    • 前缀状态 stst 表示:已经枚举的数字的前缀的最后一位,所以合法的状态总共有 9 种情况:分别是以 0、1、2、3、5、6、7、8、9 结尾的,然后对所有合法状态,增加一位 [0,9][0,9] 的数字后进行状态转移;状态转移过程中会出现两种非法状态:
    • 1)新增加的位等于 4;
    • 2)st=6st = 6,且新增加的位等于 2;
    • 除了非法状态不进行状态转移,其它状态都进行状态转移,我们只需要在 【例题1】的 数位 DP 代码基础上,修改 nextstateisEndStateValid函数即可。
    • c++ 代码实现如下:
    const int invalidstate = -123456789;
    bool isEndStateValid(stType state) {
        return true;
    }
    stType nextState(stType st, int digit) {
        if( (digit == 4) || (st == 6 && digit == 2) ) {
            return invalidstate;
        }
        return digit;
    }
    
    • 状态转移如图四-1-1所示:
      在这里插入图片描述
      图四-1-1
    • 图中红色状态代表非法状态,蓝色状态代表任意的合法状态。当一个以 3 结尾的数字,增加一位 6,再增加一位 2,则变成非法状态,无法再进行状态转移,同样,增加一位 4,也能直接变成非法状态。而一个以 6 结尾的数字,增加一位 3,则又回到了 3 的状态。
    • 可以定义一个和所有状态变量不重复的常量(一般可以用负数,如 -123456789)来表示非法状态。

    2、饱和状态

    • 所谓饱和状态,就是对于某个数字,在它的某个前缀已经能够判定这个数满足给定题目的条件,而这个前缀状态被称为饱和状态,我们通过【例题3】来理解下饱和状态的实际含义。

    【例题3】给定一个 n(1n2631)n(1 \le n \le 2^{63}-1),求小于等于 nn 的数字中包含 49 的数的个数。

    • 从题意可以得知,饱和状态 和 非法状态 正好是两个逆状态。
    • 对于任意一个小于等于 nn 的数,它的某个前缀的最后两位数字只要是 “49” 就一定是我们要求的数,换言之,只要一个数的前缀以 “49” 结尾,那么就一定是饱和前缀状态,简称饱和状态。饱和状态,无论接收什么数字,还是保持饱和状态不变。
    • 前缀状态 stst 表示:已经枚举的数字的前缀的最后一位,所以合法的状态总共有 10 种情况:分别是以 0、1、2、3、4、5、6、7、8、9 结尾的,然后对所有合法状态,增加一位 [0,9][0,9] 的数字后进行状态转移;状态转移过程中会出现一种饱和状态:
    • 1)st=4st = 4,且新增加的位等于 9;
    • 当某个状态变成饱和状态的那一刻,它前缀的最后一位也是9,为了区别原先的 9,我们可以用 10 进行编码,我们只需要在 【例题1】的 数位 DP 代码基础上,修改 nextstateisEndStateValid函数即可。
    • c++ 代码实现如下:
    const int saturatedstate = 10;
    bool isEndStateValid(stType state) {
        return (state == saturatedstate); 
    }
    stType nextState(stType st, int digit) {
        if(st == 4 && digit == 9) {
            st = saturatedstate;
        }else if(st != saturatedstate){
            st = digit;
        }
        return st;
    }
    
    • 状态转移如图四-2-1所示:
      在这里插入图片描述
      图四-2-1
    • 图中黄色状态代表饱和状态,蓝色状态代表任意的合法状态。当一个以 3 结尾的数字,增加一位 4,再增加一位 9,则变成饱和状态,无论怎么状态转移都回到自己;而一个以 4 结尾的数字,增加一位 3,则又回到了 3 的状态。
    • 可以定义一个和所有状态变量不重复的常量来表示饱和状态,由于饱和状态也是要进行记忆化的,所以不能用负数。

    3、组合状态

    • 所谓组合状态,就是几种不同维度的状态,通过整数编码映射到一个整数中,方便计算。

    【例题4】一个数的十进制包含子串 “13” 并且能被 13 整除,则被称为 B 数,求小于等于 n(n109)n(n \le 10^9) 的 B 数。

    • 心里没有点 B 数,说的就是这道题了。
    • 这个问题既要满足 饱和 又要满足 同余。
    • 所以对于这个问题,我们发现,前缀状态 stst 由两部分组成:
    • 1)已经枚举的数字的前缀的最后一位;
    • 2)已经枚举的数字前缀模 13 的值;
    • 对于第(1)种情况而言,前缀最后一位总共 0、1、2、3、4、5、6、7、8、9 这 10 种情况,并且当之前一位是 1,再枚举一个 3 时,数字达到饱和状态,可以用 10 编码;而对于第(2)种情况,可以参考 【例题1】 直接采用取模的方式,总共 13 种;
    • 那么,我们可以把 (1) 和 (2) 组合起来编码,编码成一个四位的十进制数。
    • 例如:前缀最后一位为 4,且所有前缀数字之和模13 为 9,则可以用 409 来表示状态,最大的状态表示为 1012。
    • 同样,我们只需要在 【例题1】的 数位 DP 代码基础上,修改 nextstateisEndStateValid函数即可。
    • c++ 代码实现如下:
    const int invalidstate = -123456789;
    const int saturatedstate = 10;
    const int mod = 13;
    bool isEndStateValid(stType state) {
    	return (state == saturatedstate * 100 + 0); 
    }
    stType nextState(stType st, int digit) {
        int high = st/100, low = st%100;
        if(high == 1 && digit == 3) {
            high = saturatedstate;
        }else if(high != saturatedstate){
            high = digit;
        }
        low = (low * 10 + digit) % mod;
    	return high * 100 + low;
    }
    

    4、前导零状态

    • 为什么要引入前导零状态?【例题5】会告诉你答案。

    【例题5】如果一个数的二进制表示中 0 的个数大于等于 1,则称它为 round numberround \ number,给定一个区间 [a,b](1ab109)[a,b](1 \le a \le b \le 10^9),求其中 round numberround \ number 的个数。

    • 在这个问题中,01 和 1 是同一个数,但是 01 是符合 round numberround \ number 的特征的,而 1 不符合,但是我们在利用数位 DP 求解的时候,如果没有处理前导零,就会把 1 误 当成 01 而统计成 round numberround \ number
    • 前缀状态 stst 表示:二进制表示的数前缀中,0的个数 减去 1的个数,所以状态范围为 [32,32][-32, 32]
    • 则对于任意一个状态 st,在后面加上 0 和 1 后实现状态转移;
    • 由于前导零不能作为正常零统计,所以需要加入一个初始状态,即前导零状态,只要编码不在 [32,32][-32,32] 即可,可以用 3333 来表示;
    • 我们只需要在 【例题1】的 数位 DP 代码基础上,修改 nextstateisEndStateValid函数即可。
    • c++ 代码实现如下:
    const int leadingzerostate = 33;
    bool isEndStateValid(stType state) {
    	return (state >= 0) || (state == leadingzerostate);
    }
    stType nextState(stType st, int digit) {
        if(st == leadingzerostate) {
            if(digit == 0)
                return leadingzerostate;
            st = 0; 
        }
        return st + (digit == 0 ? 1 : -1);
    }
    
    • 状态转移如图四-4-1所示:
      在这里插入图片描述
    图四-4-1
    • 图中绿色状态代表前导零状态,蓝色状态代表任意的合法状态。前导零状态的特点是遇到 0 则回到自己。
    • 对于所有前导零会影响结果的问题,我们可以采用如下通用解法:
    • 1)定义 1 个不和其它状态数字重复的 前导零状态;
    • 2)状态转移的时候,如果源状态是前导零状态,在遇到下一位是零的情况,则维持前导零状态;否则,进入下一状态;
    • 3)结束状态时判定如果是前导零状态,则表明这个状态表示的数就是 0,进行相应的判定。

    5、位压缩状态

    • 一个二进制数的每一位有两种取值 [0,1][0,1],对于一些互相独立的状态,可以用一个整数来表示各个维度的组合。总共可以表示 2n2^n 种状态。

    【例题6】一个数字的十进制下的每个位都不同,则称为 Special NumberSpecial \ Number,求小于 n(n<108)n(n \lt 10^8) 的数的个数。

    • 为了确保十进制数的每一位都不同,那么在没增加一位的时候,都需要判断新增的这个数字在之前的高位数字中有没有出现过,数字一共 10 种,有 和 没有是两种状态,所以最多就是 2102^10 种状态。
    • 前缀状态 stst 表示:每位数字的出现次数,例如 st=11st = 11 的二进制表示为(1011)2(1011)_2,代表的是0、1、3 这三个数字出现过,所以我们在进行状态转移的时候,遇到 0、1、3 只能进入非法状态。
    • 实际实现可以采用位运算加速,我们只需要在 【例题1】的 数位 DP 代码基础上,修改 nextstateisEndStateValid函数即可。
    • c++ 代码实现如下:
    bool isValidState(stType state) {
        return state != leadingzerostate;
    }
    
    stType nextState(stType st, int digit) {
        if(leadingzerostate == st) {
            if(digit == 0)
                return leadingzerostate;
            st = 0;
        }
        if( st & (1<<digit) ) {
            return invalidstate;
        }
        return st | (1<<digit);
    }
    
    

    6、二分优化

    【例题7】一个数如果至少包含 3 个 6,则称为 “beast number”,给定一个 kk, 求第 kk 个 “beast number”。

    • 假设 [0,x][0, x] 区间内有 g(x)g(x) 个满足条件的数,那么自然,g(x)g(x) 是 关于 xx 的单调不降函数。
    • 我们只需要求出满足 g(x)kg(x) \ge k 的最小的 xx 就是答案了,于是可以用 二分 xx,数位DP 求解 g(x)g(x),从而找出最小的 xx
    • 状态编码如下:
    • 状态0:前缀数字最后位不是6,且未出现过连续3个6;
    • 状态1:前缀数字最后位连续6的个数为1个,且未出现过连续3个6;
    • 状态2:前缀数字最后位连续6的个数为2个,且未出现过连续3个6;
    • 状态3:已经出现过连续3个6,饱和状态;
    • 状态转移如图四-6-1所示:
      在这里插入图片描述
      图四-6-1
    • 然后就是 二分答案 + 数位DP 判可行 了。
    • 我们只需要在 【例题1】的 数位 DP 代码基础上,修改 nextstateisEndStateValid函数即可。
    • c++ 代码实现如下:
    const int saturatedstate = 3;
    bool isEndStateValid(stType state) {
    	return (state == saturatedstate);
    }
    
    stType nextState(stType st, int digit) {
        if(st == saturatedstate) {
            return saturatedstate;
        }
        if(digit == 6) {
            return st + 1;
        }
    	return 0;
    }
    

    五、数位 DP 总结

    1、状态转移图

    在这里插入图片描述

    图五-1-1

    • 图五-1-1对四类状态进行了一个动态演示,接下来对这张图做一个简单的解释:
    • 前导零状态●:接收 数字0 时回到自己,否则根据题意进入任意初始状态;
    • 饱和状态●:接收任意数字都回到自己;
    • 非法状态●:无法再进行状态转移;
    • 其它状态●:进行正常状态转移的状态,可能到饱和状态,也可能到非法状态,但是无法回到前导零状态;

    2、数位 DP 模板


    • 关于 数位DP 的内容到这里就结束了。
    • 如果还有不懂的问题,可以 想方设法 找到作者的微信进行在线咨询。


    六、数位 DP 相关题集整理

    题目链接 难度 解析
    HDU 5965 扫雷 ★☆☆☆☆ 无数位限制的简单数位 DP
    HDU 4608 I-number ★☆☆☆☆ 比较水
    HDU 4722 Good Numbers ★☆☆☆☆ 【例题1】同余
    HDU 2089 不要62 ★☆☆☆☆ 【例题2】非法状态
    LeetCode 600 不含连续1的非负整数 ★☆☆☆☆ 非法状态
    HDU 3555 Bomb ★★☆☆☆ 【例题3】饱和状态
    HDU 3652 B-number ★☆☆☆☆ 【例题4】饱和状态 + 同余
    PKU 3252 Round Numbers ★★☆☆☆ 【例题5】前导零状态
    PKU 3286 How many 0’s? ★★☆☆☆ 前导零状态
    LeetCode 233 数字 1 的个数 ★★☆☆☆ 前导零状态
    HDU 3485 Count 101 ★★☆☆☆ 非法状态
    HDU 3284 Adjacent Bit Counts ★★☆☆☆ 非法状态
    HDU 1663 The Counting Problem ★★☆☆☆ 前导零状态
    洛谷 P2602 数字计数 ★★☆☆☆ 前导零状态
    洛谷 P2657 windy 数 ★★☆☆☆ 前导零状态 + 非法状态
    洛谷 P3413 萌数 ★★☆☆☆ 饱和状态 + 前导零状态
    洛谷 P6754 Palindrome-Free Numbers ★★☆☆☆ 非法状态 + 前导零状态
    洛谷 P4317 花神的数论题 ★★☆☆☆ 二分快速幂 + 数位DP
    HDU 5898 odd-even number ★★☆☆☆ 前导零状态 + 非法状态
    HDU 6148 Valley Numer ★★☆☆☆ 非法状态 + 前导零状态
    洛谷 P6371 V ★★★☆☆ 同余 + 分情况讨论
    HDU 4734 F(x) ★★★☆☆ 预处理 + 剪枝
    HDU 4151 The Special Number ★★★☆☆ 【例题6】位压缩 + 非法状态 + 前导零状态
    HDU 5179 beautiful number ★★★☆☆ 位压缩 + 前导零状态
    洛谷 P4124 手机号码 ★★★☆☆ 位压缩 + 前导零状态
    洛谷 CF855E Salazar Slytherin’s Locket ★★★☆☆ 位压缩 + 前导零状态
    PKU 3208 Apocalypse Someday ★★★☆☆ 【例题7】二分 + 饱和状态
    HDU 2867 Continuous Digit ★★★☆☆ PKU 3208 的一般情况
    HDU 3271 SNIBB ★★★☆☆ 二分 + 求和
    HDU 2889 Without Zero ★★★☆☆ 二分 + 非法状态
    HDU 3943 K-th Nya Number ★★★☆☆ 二分 + 非法状态
    PKU 3971 Scales ★★★☆☆ 进位模拟
    洛谷 P4127 同类分布 ★★★☆☆ 枚举 + 同余
    HDU 5787 K-wolf Number ★★★☆☆ 状态压缩 + 前导零状态
    HDU 3709 Balanced Number ★★★☆☆ 推导 + 前导零状态
    HDU 3967 Zero’s Number ★★★☆☆ 组合状态
    HDU 5676 ztr loves lucky numbers ★★★☆☆ 二分 + 数位DP
    HDU 5456 Matches Puzzle Game ★★★★☆ 数位 DP 进阶题
    洛谷 CF55D Beautiful numbers ★★★★☆ 最小公倍数 + 数位 DP
    HDU 4507 吉哥系列故事——恨7不成妻 ★★★★☆ 数论 + 数位DP
    HDU 4352 XHXJ’s LIS ★★★★☆ 最长递增子序列 + 数位DP
    PKU 3986 Math teacher’s homework ★★★★★ 位运算 + 数位DP
    展开全文
  • 如何从开始一个操作系统?

    万次阅读 多人点赞 2019-08-20 18:17:55
    如何从开始一个简单的操作系统? 关注问题 回答 操作系统 编程学习 如何从开始一个简单的操作系统? 看了这个:从开始一个简单的操作系统 求指教。 关注者 4,787 被浏览 352,884 关注问题 ...

    登录加入知乎

    如何从零开始写一个简单的操作系统?

    关注问题

    写回答

    操作系统

    编程学习

    如何从零开始写一个简单的操作系统?

    看了这个:从零开始写一个简单的操作系统 求指教。

    关注者

    4,787

    被浏览

    352,884

    关注问题

    写回答

    ​邀请回答

    ​3 条评论

    ​分享

    37 个回答

    默认排序​

    知乎用户

    知乎用户

    751 人赞同了该回答

    终于可以来回答这道题了……

    一年多前,也就是大一下学期末的时候,我看到这个问题下

    @fleuria

    叔的答案,然后看了 F 叔给的这个链接 基于 Bochs 的操作系统内核实现 ,当然是什么都看不懂,除了惊诧之外也了解了一件事情:一个人写一个简单的操作系统内核是一件非常帅气并且可行的事情。

    于是我开始写了,那时候我的水平大概是:只会做 C 语言的习题,编译的话只知道按 F9,汇编知道常见的指令,另外会一点点的 Win 32 编程,能流畅使用 Windows。

    一开始我找了《30 天自制操作系统》来看,每天看书,然后把从书里把代码打出来,一次一次地编译运行。因为要同时写汇编和 C,所以从那时候起就开始用 vim。

    在啃完了差不多半本书后,开始觉得没意思了……因为觉得作者为了让内容更简单而省略了太多细节。也看了于渊的《Orange‘s 一个操作系统的诞生》,依然没看下去:汇编用得太多了。期间也曾斗胆发邮件给 F叔,然后他推荐了 Bran's Kernel Development Tutorial 这个教程,于是我就从这教程重新开始了: 「30天自制操作系统」 Stop & 「OS67 」 Start

    那时候大概是大二上学期,于是在 github 上又开了一个 repo,一开始在 Windows 下开发,后来又切换到了 Linux 下,因为 Bran's 用的 bootloader 是 Grub,不符合我的初衷,所以就自己写了一个,之后便跟一路教程写,跨过了保护模式这道坎,完成了基本的设备驱动。

    在完成 Bran's 后,我又部分参考了 写一个操作系统内核有多难?大概的内容、步骤是什么? - To浅墨的回答 中推荐的:hurley25/hurlex-doc · GitHub 文档,完成了一些简单的调试函数和库函数,printk 和内存分配。
    事实证明,尽早写好调试函数诸如 panic, assert 和 printk 是非常重要的。 大量地使用有助于你尽快地发现 bug (当然前提是这些函数本身不能有 bug)。

    看完了 hurlex-doc 该看的部分后,很长一段时间了都不知道该干嘛好,模仿 hurlex-doc 里的内核线程切换也一直出错。这一情况一直持续到我开始读 Xv6, a simple Unix-like teaching operating system

    如果你去看知乎关于「自制内核」的问题,你会发现 xv6 被反复地提及并推荐,事实上它非常值得被推荐:这是我读完大部分代码之后真切体会到的。

    之前的 Bran‘s 和 hurlex-doc 的篇幅都比较小,我是在电脑和 kindle 上看完的,xv6 相对来说代码量比较大,有 9000+ 行和一份文档,之后我又找到了这个:ranxian/xv6-chinese · GitHub xv6 文档的中文译版,所以我就去花了十二块钱学校打印店打印了一份中文文档和一份代码。这又是一个正确的决定,让我不必对着电脑就能看代码。

    在之后的时间里,我先读了 xv6 中文件系统相关的部分,然后改写它的代码为我的内核添加了一个 类似 Minix 的文件系统。 然后几乎又照抄了其中了进程调度的部分(做了部分简化),又在原来的代码基础上为添加操作系统的接口,接着写用户程序,过程几乎是「一路顺风」。看 xv6 的那段时间也经常是处于醍醐灌顶的状态。

    最后我终于在差不多一个月前完成了这个简陋的操作系统内核:
    LastAvenger/OS67 · GitHub (没错其实我是来骗 star 的)

    历时一年,一路点亮了不少技能树(虽然都点得不好),这样算是「从零开始写一个简单的操作系统」么? 跟进一步说,有谁不是从零开始的呢? 所以想做的话,现在就开始做好了。

     

    这是被「翻烂」了的 xv6 源代码和中文文档(其实是放书包里被磨烂了)


    「故事」讲完了,接下来说一点经验之谈吧……

     

    * 知乎上总是有人在讨论「做一个玩具编译器和做一个玩具内核何者更有趣」之类的问题,然后总有各种大V 跳出来说内核有多 dirty 而编译器多 clean,事实上除了 CPU 上的几个表因为历史原因长得恶心一点,内核并没有什么特别 dirty 的地方,另外,想做点什么打发时间,不过是两个代码量稍多的入门项目,有什么好纠结的?
    * 写内核的过程中,你会接触到一些这辈子大概只会用到一次的知识,A20 线已经成为历史,日常的编程里面也不需要你懂得 GDT IDT 的结构。但是单凭内核主要部分部分(文件系统,进程,内存)给你带来的知识而言,这点冗余是值得的。
    * 尽早实现调试函数并大量使用,善于利用 bochs 的内置调试器,能省下你不少时间。
    * 有时候觉得书里的做法非常奇怪,你觉得你有更好的做法,一般是你想错了。(当然只是一般)
    * 上面说看 xv6 一路顺风是假的,20% 时间在抄代码,80% 的时间用来调试
    * 对我这种能力一般的人来说,「写内核」只是好听的说法,正确的说法是「抄内核」。当然,就算是抄一个,也算是受益匪浅了。
    * 抄 xv6 的好处在于,即使你的代码出错了,你可以坚信,正确的答案肯定在 xv6 的代码里,或许只是你还没理解透而已,只要不断地看和理解,你就离正确的道路越来越近。

    最后,感谢

    @fleuria

    在微博和邮件里的多次帮助,

    @To浅墨

    的 hurlex-doc 文档,鲜染同学翻译的 xv6 中文文档,

    @郭家华

    完美地解答了我一开始的疑问,让我在内核中得以使用 C 语言。
    在 #archlinuxcn 频道里也得到了很多人的帮助。

    发布于 2015-11-09

    ​赞同 751​​54 条评论

    ​分享

    ​收藏​感谢收起​

    邱永臣

    邱永臣

    什么都懂一点,同时又什么都不懂,这就是我,一个喜剧演员。

    1,430 人赞同了该回答

    大二的时候,老师(中山大学万海)对我们说:“如果有谁能自己写一个内核出来,那么,他平时可以不来听课,也不用做平时作业,做出来还能加分,怎么样,有没有人有兴趣?”

    和老师一番讨价还价之后,我成为全年级几百号人里唯一一个自己写内核/整个学期都不去教室听课/任何作业都不做的那个人(代表着我的身边将没有可以提供参考的人,任何资料都只能自己找)。

    一开始买了《30天自制操作系统》,上面写着需要软盘还有其它的模拟器,我的初衷是写一个可以烧在真机上一按开机键就能跑起来的那种,所以看了几页后就丢开了。后来又找了国人写的一本,也不是特别符合,也丢开了。

    这时我看到了那本教材(俗称绿宝书),约莫800页。之后的两个星期里,我每天泡图书馆,以每小时10页的速度读完了它,在上面乱涂乱画了许多标记。800页的英文书,我从中学到了大量的基本概念(线程进程,内存算法,寻址方式等等)。

    接着我寻思直接从网络上而不是从书上寻找资料,TA师兄给我提供了一个OS Development,我照着上边的例子,写了数以千记的汇编代码,习得了汇编技能。

    此时,我具备基本的概念知识,对程序的语言也已经理解,知道了虚拟机的调试方法,差的,就只有对内核整体是如何协作不太明白。于是我去找来老师用于教学的PintOS,找来MIT那个项目的代码,还有国内一个高校自制的OS(是几个研究生一起写的),仔细研究了一遍,最后开始写代码。

    在那个学期里,我放弃了LOL,一心看代码,写内核,写各种模块,将过程记录在博客上,花了三个月的时间,最终写出一个具备terminal的内核(文件系统没写好,时间不够),可以跑命令,运行函数,管理内存和进程,处理中断。






    如果你想知道具体整个编写的过程是怎样的,可以看看我当时的记录,如下(很长):

    原文:(http://www.ilovecl.com/2015/09/15/os_redleaf/ )







    (一)OS说明

    今后,我就要开始折腾操作系统,有了一点小小干劲。

    我的计划是,先看过一份用于教育目的的系统源码,再去翻找相应的资料(我手头已有绿宝书),在翻资料的同时开始写代码,然后做好移植真机的工作,DONE!
    我也明白,理性很丰满,现实很骨感,这过程不会如同我计划中这般简单和轻松。但是,见难而退可不是我的风格(那样我会被红叶二小姐调戏的),不管如何,我都会,怎么说呢,尽力吧。

    出于课程需求,斯坦福那些人亲自写了一个名为“pintos”的系统。pintos的结构比较简单,分为进程管理、文件系统、用户程序、虚拟内存等几个部分,也正是因为这个原因,我选择pintos作为我的参考蓝本,现在在读它的源码。

    在接下来的几个月时间里,不出意外的话,我会不断的在博客上更新我的进度。

    (三)交叉编译环境

    倘若我们要在ubuntu上编译另外一个完整的OS,交叉编译环境是必不可少的玩意,维基百科有云:

    交叉编译器(英语:Cross compiler)是指一个在某个系统平台下可以产生另一个系统平台的可执行文件的编译器。
    (想起以前,我为了给路由器编译OPENWRT,下载大量源码,愣是编译了几天几夜。那时候的我,真是“可爱”。)
    为了配置好交叉编译环境,我废了好大力气,最后勉强找到了组织。
    编译环境大致分为2部分,binutils和gcc。我先装好gcc-4.9.1,之后下载gcc-4.9.1和binutils-2.25的源代码,似乎gcc版本与binutils版本要对应来着…

    开始编译之前,需要准备全局变量(在命令行中敲入以下命令):

    export PREFIX=”$HOME/opt/cross”
    export TARGET=i686-elf
    export PATH=”$PREFIX/bin:$PATH”
    编译Binutils
    cd $HOME/binutils-2.25
    mkdir build-binutils
    cd build-binutils

    #注意是在源码目录下面新建一个文件夹,然后cd到该文件夹里,然后才配置configure,不这么做的话,嘿嘿..
    ../binutils-x.y.z/configure –target=$TARGET –prefix=”$PREFIX” –with-sysroot –disable-nls –disable-werror
    make
    make install
    –disable-nls 告诉binutils,不要添加本地语言支持

    –with-sysroot 告诉binutils,在交叉编译器中允许sysroot

    编译GCC
    cd $HOME/gcc-4.9.1
    mkdir build-gcc
    cd build-gcc

    #注意是在源码目录下面新建一个文件夹,然后cd到该文件夹里,然后才配置configure,不这么做的话,嘿嘿..
    ../gcc-x.y.z/configure –target=$TARGET –prefix=”$PREFIX” –disable-nls –enable-languages=c,c++ –without-headers
    make all-gcc
    make all-target-libgcc
    make install-gcc
    make install-target-libgcc
    –disable-nls 告诉GCC,不要添加本地语言支持。

    –without-headers 告诉GCC,不要依赖任何本地库,我们必须在自己的OS中实现库。

    –enable-languages 告诉GCC,不要支持除了C、C++之外的语言。

    提醒
    不同机器配置不同,编译速度也不同。

    编译这两个软件,我花了近3个钟,机器配置之低自不必说,说了都是泪。

    如果任何人的任何编译过程出了任何问题,请仔细地、认真地、用心地再看看上面的命令,在你没有弄懂它的原理之前,请不要擅自做任何“改进”(血淋淋、赤裸裸的教训呀)。

    (五)OS模糊框架

    翻完了手头的绿宝书,我才晓得,人都是被逼出来的。

    操作系统的概念都差不多已经知道,接下来,该由“理论态”切换到“实践态”了喔(书还是不能看太多,会中毒的–)。

    对了,从别人推荐的地方弄来了一个框架(曾在android平台写了几万代码,我深深体会到框架的作用),轻松开工吧。

    先说明一下这个框架:Meaty Skeleton,开源示例,内核和用户分离,方便扩展,嗯,没了。

    最近烦杂事情很多,心情,不算愉快也不算低落吧,近来又梦见红叶,不知道又要发生什么,不管。

    (六)内核第一步任务:GDT完成

    天色已晚,又下着雨,我也忘记带伞了,嗯,等会儿再回去好了,这个商城的环境还是蛮好的。

    今天实现了GDT。

    (也不算是实现吧,因为我打算使用纯分页的流氓招数,放弃纯分段或分段分页混合,所以就不太用心于实现GDT,只是浏览INTEL的官网,借用了几个FLAG定义之类的东西,匆匆就写完了GDT)

    下面是记忆:

    使用内嵌式汇编
    分4个段,两个高级的内核分段,两个低级id用户分段
    预留了一个TSS,虽然也不打算用硬件实现任务切换(听前辈们说,硬件实现非常的麻烦)
    把 设置GDT表的函数(init_gdt)放在kernel/arch/i386/global_descriptor_table.c中,而段 segment_descriptor的定义(seg_desc)则放在kernel/include/kernel /global_descriptor_table.h
    引用了英特尔的一份公开资料
    一些全局或者说全世界通用的参数放在kernel/include/kernel/global_parameter.h,有些人更绝,把所有函数的原型放在一个地方,哪怕内核级函数和用户级函数混在一起
    翻了太多资料,头都晕了
    按进度来看,有点紧,也无妨。

    (七)内核第二步任务:IDT完成

    佛说人者,非人者,名人者。
    已经写好IDT的载入,加上之前的GDT载入,就已经完成两个与机器硬件相关的模块(准确的说,应该是给CPU的特定单元载入内容)。不过我并没传说高手那么厉害,高手们一天一个模块,可我近几天连IDT对应的IRC和HANDLE都还没弄。

    在bochs上调试时,分别 键入info gdt 和info idt 1,能看到GDT和IDT的内容。

    今日要点:

    AT&T汇编和寻常的INTEL有些许区别,不过区别不是很大
    GDT和IDT都是固定的表,必须实现,实现方法各异
    之前留下的TSS并非用于切换任务,而是用于保存从“用户态”回到“内核态”时必须使用的跳转地址
    未完待续
    后记,IDT里面的OFFSET并没有得到正确的值,因为IRQ还没设置好,相应的HANDLE还没有弄好
    2015年4月16日01:14:25补充:

    设 置了IDT表中的头32个项,也就是ISR(interrupt service routines),它专门处理诸如“除以0”/“Page Fault”/“Double Fault”等exception,它对exception的处理方式也很简单,或者说根本没有处理,仅仅是打印exception的类型而已。

    我 随便写了一句int a = 1/0,调试的时候,bochs提示”write_virtual_checks(): no write access to seg”。可能是内核还没具有从用户态跳转到内核态的能力吧,毕竟IDT的头32个项都拥有ring0的级别,明天再看看。

    补上3种中断类型:

    Exception: These are generated internally by the CPU and used to alert the running kernel of an event or situation which requires its attention. On x86 CPUs, these include exception conditions such as Double Fault, Page Fault, General Protection Fault, etc.
    Interrupt Request (IRQ) or Hardware Interrupt: This type of interrupt is generated externally by the chipset, and it is signalled by latching onto the #INTR pin or equivalent signal of the CPU in question. There are two types of IRQs in common use today.IRQ Lines, or Pin-based IRQs: These are typically statically routed on the chipset. Wires or lines run from the devices on the chipset to an IRQ controller which serializes the interrupt requests sent by devices, sending them to the CPU one by one to prevent races. In many cases, an IRQ Controller will send multiple IRQs to the CPU at once, based on the priority of the device. An example of a very well known IRQ Controller is the Intel 8259 controller chain, which is present on all IBM-PC compatible chipsets, chaining two controllers together, each providing 8 input pins for a total of 16 usable IRQ signalling pins on the legacy IBM-PC.
    Message Based Interrupts: These are signalled by writing a value to a memory location reserved for information about the interrupting device, the interrupt itself, and the vectoring information. The device is assigned a location to which it wites either by firmware or by the kernel software. Then, an IRQ is generated by the device using an arbitration protocol specific to the device’s bus. An example of a bus which provides message based interrupt functionality is the PCI Bus.
    Software Interrupt: This is an interrupt signalled by software running on a CPU to indicate that it needs the kernel’s attention. These types of interrupts are generally used forSystem Calls. On x86 CPUs, the instruction which is used to initiate a software interrupt is the “INT” instruction. Since the x86 CPU can use any of the 256 available interrupt vectors for software interrupts, kernels generally choose one. For example, many contemporary unixes use vector 0x80 on the x86 based platforms.
    今天载入到IDT中的,正是第一种类型(Exception),只不过换了个名字叫ISR而已。

    未完待续。

    2015年4月18日12:06:45补充:

    之前的”write_virtual_checks(): no write access to seg”错误并不是权限的问题,而是段寄存器DS的值错误,它的值应该是0x10,可我给它赋值0x08。0x08是段寄存器CS的值,0x10才是段寄存器DS的值。

    另外,这at&t汇编里面,把C语言函数的地址赋给寄存器,必须在函数名前面加上$。

    至此,ISR彻底完成,只是,似乎IRQ又出了点问题….

    未完待续。

    (十)内核第三步任务:分页完成

    稍微做下记录…

    得到内存大小
    首先,利用grab得到物理内存的实际大小。

    物理内存管理
    然后,用一个数组map来监督物理内存,数组的每一项都对应着一个4K的物理内存。在这里我遇到了一个问题:数组的大小如何设置?因为还没有内存分配功能,所以不可能allocate一块或new一块内存来存放数组。找来找去也没找到合适的方案,就自己弄一个粗鲁一点儿的:设置数组大小为1024 1024。这样一来,数组的每一项对应4K,有1024 1024项,恰好可以对应4G大小的物理内存。但这样又有一个缺陷,倘若物理内存没有4G而是128M,那么该数组就有大部分元素被废弃了。现在先,额,不管这个,之后再解决。

    至于这物理内存它的实际分配,我是这么觉得的:把前64M的物理内存当作内核专属(把内核的所有内容全都加载到此处),剩余的物理内存才是空闲内存,用于allocate。

    为了方便分配物理内存,我采取最最最简单的方法:把所有空闲的物理页放到一条链里,需要的时候直接拿出来就可以了。

    虚拟内存管理
    之后,就是把page_directory地址放入CR3并开启硬件分页功能了。

    page_directory,page_table等作用于虚拟地址。对于这4G的虚拟地址空间,排在前面大小为MEM_UPPER的一大块虚拟内存都是内核空间,剩下的排在后面的都是用户空间。也就是说,在有512M的物理的情况下,虚拟内存的前512M是内核态,后面的3584M是用户态。

    分页错误
    内存分配的过程中,可能出现“页面不存在”、“页面只读”及“权限不足”3种错误。处理分页错误,CPU会自动调用14号ISRS,我们要做的,是把我们写的处理函数地址放到14号ISRS的函数栏即可。

    每次分页错误,CUP调用14号ISRS,继而跳入我们设计好的处理函数(-_-陷阱?)。

    不过我现在也是暂时先不写分页错误的处理函数,如果内存真的任性真的出错了,我也不会管它的,傲娇就傲娇吧。

    到这里,分页就算是初步完成了。

    致命的伤痛
    很遗憾,物理内存设置好了,虚拟内存设置好了,也正常工作了,但是我一旦开启硬件的分页功能,就有”physical address not available”的错误,直接重启了,到底是怎么回事…再看看吧…

    未完待续。

    2015年5月1日12:54:14补充:

    bochs的”physical address not available”提示是这么个回事,把一个内容不对的分页目录加载进硬件(也就是把分页目录地址置入CR3)。在初始化分页目录时,我直接用了4M大页的方式初始化,但弄错了byte和KB的数量级,所以就出了一点小小的问题。

    遗留:page fault函数,待日后再写。

    写内存分配去吧!

    未完待续。

    (十一)内核第四步任务:内存分配完成

    内存分配?这可是个麻烦的活,不过,如果你足够聪明的话,就没什么问题了。 ——前人
    上 一次,我准备好了分页的相关内容,比如说,载入分页目录/开启硬件支持/划分物理内存/划分虚拟内存等等。这一次,不会怂,就是干(为写内存分配模块而奋 斗,高扛自由的鲜红旗帜,勇敢地向前冲….)。分页准备好之后,下一步是如何地分配内存,比如,如何分配一页空白的可用的物理内存?如何分配一块空白 的虚拟内存?如何连续地分配等等等等。
    第一节:申请和释放空白物理内存
    申请物理内存,在分页的机制下,就是申请一页或连续几页空白的物理内存,释放则反过来。

    在 分页的时候,我已经将所有的空白物理页都放进了一个链表之中,现在要申请一个空白物理页,从链表中拿出来即可,太简单了。释放空白物理页,将物理页重新放 进链表里即可,也是非常的简单,有点简单过头了。当然啦,简单有省时省力的优点,同时,也有“无法同时分配许多页/分配大内存时(比如数十M)很吃力”的 缺点。这,按我的习惯,先留着,以后再说,现在能简单就简单。

    写好allocate_page和free_page两个函数之后,分配空白页倒是正常,但是内核出现”double fault”的错误,也就是8号ISR被CPU调用了,具体为甚,现在还不清楚,待我瞧瞧再说。

    未完待续。

    查资料如下:

    Normally, when the processor detects an exception while trying to invoke the handler for a prior exception, the two exceptions can be handled serially. If, however, the processor cannot handle them serially, it signals the double-fault exception instead. To determine when two faults are to be signalled as a double fault, the 80386 divides the exceptions into three classes: benign exceptions, contributory exceptions, and page faults. Table 9-3 shows this classification.

    Table 9-4 shows which combinations of exceptions cause a double fault and which do not.

    The processor always pushes an error code onto the stack of the double-fault handler; however, the error code is always zero. The faulting instruction may not be restarted. If any other exception occurs while attempting to invoke the double-fault handler, the processor shuts down.

    ————————————————————————–

    Table 9-3. Double-Fault Detection Classes
    Class ID Description

    1 Debug exceptions
    2 NMI
    3 Breakpoint
    Benign 4 Overflow
    Exceptions 5 Bounds check
    6 Invalid opcode
    7 Coprocessor not available
    16 Coprocessor error

    0 Divide error
    9 Coprocessor Segment Overrun
    Contributory 10 Invalid TSS
    Exceptions 11 Segment not present
    12 Stack exception
    13 General protection

    Page Faults 14 Page fault
    ————————————————————————–

    Table 9-4. Double-Fault Definition
    SECOND EXCEPTION

    Benign Contributory Page
    Exception Exception Fault
    Benign OK OK OK
    Exception

    FIRST Contributory OK DOUBLE OK
    EXCEPTION Exception

    Page
    Fault OK DOUBLE DOUBLE
    ————————————————————————–

    大概意思是:同时出现了2个中断,CPU不知道该处理哪个先,就是这样,就是如此的简单。之前没有这个错误,但分配和释放几个物理页之后就有这个问题,我估摸着两个都是Page fault,再看看吧。
    刚刚调试了一下,我发现不是分配和释放几个物理页的问题,而是cli()和sti()的成对出现,去掉它们就没这个问题;更奇怪的是,就算只有sti() 允许中断出现,也会double fault,莫非我这前面关了中断或者是前面遇到了不可解决的中断遗留到现在?难道,是irq的重定位有问题?到底是为什么呢?先算入历史遗留问题吧,还 有重要的模块要完成。
    (事情有点麻烦了呢?并不是内存分配这里出了问题,而是sti()惹的祸,不管这哪个位置,只要调用sti()开启中断,就会double fault,看来必须解决这个问题才行,我不可能一直不开中断吧…-_-)
    睡了一觉,起来查资料,看到了关键的一句:make sure you didn’t forget the CPU-pushed error code (for exceptions 8,10 and 14 at least)到了,我翻出代码一看,哎呀嘛,我只注意到了8号软中断,没注意到10号和14号软中断(14号处理page fault),删去两行代码后,顺利开启中断!
    未完待续。
    第二节:分配内存(malloc/free)
    既然已经可以正常地分配和释放物理内存页,那么在这一小节之中,很自然地,我的任务就是分配内存了。

    所谓“天将降大任于斯人也,必先让他实现一个内存分配的算法”,不外乎就是说,要实现void malloc(int size)和int free(void p, int num_page)两个大众情人函数。

    它 的大概思路就是这样的:先初始化一个桶,把可用的内存块都塞进去,要分配内存时,直接从桶里面找,找到了当然万事大吉大家都开心,如果找不到,就调用上面 那个申请空白的物理内存页的函数,弄一个4K物理内存页过来,将这个内存页分割成小块,丢到桶里面,然后继续找,就是这样….
    2015年5月5日23:19:08补充:

    遇到一个bug:每次申请的时候,可以正常申请,但是一旦使用了申请的内存,内核就报”page fault”的错误。想来想去,看来看去,最终发现,我在初始化分页机制的时候出了点小小的问题。

    秘技解决:

    初 始化虚拟内存时,我将大小和物理内存一样大(比如129920K)的虚拟内存设为内核级别并可用,剩下3个多G的虚拟内存是用户级别但不可用,我使用4M 大页载入分页表,所以我实际上载入了129920/4096 = 31个大小为4M可用的内核级别虚拟内存页,也就是说,在虚拟内存这个空间里,仅仅有31 4096 = 126976K的可用空间,其它的虚拟内存均是不可用的非法的;而在初始化物理内存时,我将前64M留给内核,后面的物理内存用于malloc和 free,比如有129920K,我把它划分为129920 / 4 = 32480个4K大小的物理内存页,也就是说,在物理内存这个空间里,仅仅有32480 4 = 129920K的可用空间,其它的物理内存均不在管理范围之内;这样一来,就出大问题了。

    假设我们要申请一个物理页,由于使用链的方式管理物理页,申请到的就是排在后面的物理内存,比如申请到了129916K到129920K这一个物理内存页,现在,我们要使用它,会发生什么呢?page fault!!!!!!!

    为 什么?很明显,在虚拟内存的空间里,最大的有效内存是126976K,CPU的分页表里只能找到前126976K,现在让CPU去找129916K,它根 本就找不到!它以为这个虚拟地址并没有对应这物理地址,是个错误!(附上page fault的引发条件:A page fault exception is caused when a process is seeking to access an area of virtual memory that is not mapped to any physical memory, when a write is attempted on a read-only page, when accessing a PTE or PDE with the reserved bit or when permissions are inadequate.)
    于是我稍作改正,就正常了,可以正常使用申请到的内存-_-。

    未完待续。

    (十二)内核第五步任务:系统时钟中断、键盘中断

    我现在的状态不是很好,刚弄好系统时钟中断,每10ms发出一个中断请求;但键盘中断并没有弄好,没有识别键盘的按键SCANCODE,所以暂时只能识别第一次按键,系统收不到第二次按键中断,明个儿我再来看看,已经很晚了--!!
    未完待续。

    2015年5月9日 15:51:00补充:

    查了一番资料,调试了一番,现在,键盘中断正常工作了,键盘可以正常工作,每输入一个字符,就在屏幕上显示出来。

    嗯哼,可以进入到进程模块了。

    (十三)内核第六步任务:进程创建

    在自习室里,我突然想到一个问题:一个进程,如何去创建它?(虽然之前翻完了大宝书,但毕竟一个多月都过去了,忘了具体的实现-_-)

    翻 翻书,找到一个和我的设想相差不多的方案:用一个特定的结构体代表一个进程,结构体中包含进程的相关信息,比如说进程的pid、上下文、已打开的文件、优 先级、已经占用的CPU时间、已经等待的时间、虚拟内存空间、错误码等等,创建进程的时候,只需要跳转到进程的虚拟内存空间即可。至于如何跳转,那就是内 核态的事情了,一般的进程都处在用户态,也就不必关心太多。

    如此,我们便是可以创建并运行一个进程了(不考虑文件系统),既然可以创建进程,可以切换进程,那么进程调度就很容易了,不过就是个复杂的进程切换过程而已,下一节便是写进程的调度罢。

    (十四)内核第七步任务:进程切换与进程调度

    黄粱一梦。
    看到这句古语,顿时感慨万千,没想到仅仅数周时间,我的人生竟发生了这么大的转折(不是一夜暴富),仿佛一夜醒来,到另外一个平行世界里去。甚至,在睡梦中我都会惊醒。

    逝 者已逝,再多的话语也没用。只是,我不甘愿就这么结束而已。她也曾经说过:“此身不得自由,又何谈放纵”,现在我竟是极度赞同了。曾经想过在割腕的那一瞬 间,她的脑海里究竟有什么,有没有浮光掠影,有没有回放这一生的片段?如此年轻的生命,选择自我了断,需要多少黑暗沉淀,多少的落寞与失望…似乎一下 子也看开了。

    (以上只是个人情感的流露,忍不住必须得写些什么,请忽略)

    简单记录一下吧,没什么心情。

    进程切换时,只需要切换进程上下文,把context刷新一遍即可。

    至于进程调度,这个就简单许多了(其实也挺复杂),在时钟中断到来的时候,调整各个进程的优先级,并切换到相应的进程,就是这么简单。

    嗯,就这样吧,现在只想戴上耳机听听音乐….

    编辑于 2016-05-12

    ​赞同 1.4K​​108 条评论

    ​分享

    ​收藏​感谢收起​

    梦人亦冷

    梦人亦冷

    嵌入式/竞赛达人/cs大法好

    492 人赞同了该回答

    我来写一个如何在15天内完成一个嵌入式实时操作系统,并移植到stm32单片机的攻略吧。第一次看到这个问题是在大概两个月之前,从那时候开始决定自己也写一个操作系统,于是开始看os的基本概念,进程切换,任务调度,内存管理,任务通信,文件系统等等。


    前言:
    大约在两个周不到前动手,平均每天7个小时,从完全不知道怎么下手(真的快哭了),到现在完成了一个----基于优先级时间片调度,具有信号量,消息队列,内存管理的嵌入式系统并命名为--LarryOS,并且移植到stm32(Cortex-M3架构)上,还在stm32上实现了一个malloc和free函数,受益匪浅。现在还正在完善。我是用自己命名的,当然,大家写好了也可以用自己命名,图个开心么 ,想想是不是很激动啊。

    对于这个问题下的回答的看法:
    大神真的好多,几乎都是x86型的,难度真的要比我的大,不得不承认。不过,我觉得对于新手,写那样一个系统真的是太艰辛了,比如我这种入ee坑的在校大三学生,课真的太多了,周内最少三节课,而且和cs没有一毛钱关系,课余时间太少,如果花费一个学期或者三个月的时间来写,可能有些得不偿失。因为许多想写os的朋友,和我一样,一是觉得写os很酷,二是想通过写来理解os,毕竟看书看了就忘,也没有衡量自己学的好坏的标准。因此,选择写嵌入式系统,是一个非常好的选择 。

    知识储备:
    这个问题想必是很多同学顾虑的,到底写一个嵌入式系统需要什么知识储备?我从自己的经历出发,并加以简化,大家可以参考一下。

    自身版:
    1c语言要求:我自己在大学里几乎没学过c语言,上机几乎10道题会2道,期末全靠背。后来开始自学了c,看了c语言程序设计现代方法,c和指针 c专家编程 第一本书的课后题,做了3分之2吧,就这样,没了
    2汇编要求:会基本的x86指令,仅仅是基本,看了王爽的汇编语言,程序写的很少,仅仅上机课写过,不过能很快写出来。
    3微机原理或者计算机组成原理:老师上课太坑了实在,我自己看了csapp的前四章大概,豁然开朗,推荐这个。
    4操作系统:看了三个礼拜os的基本概念,仅仅是了解概念,没办法深入,也没地方。。。。
    5数据结构:看过浙大的数据结构公开课的3分之2,会写基本的链表,队列,最基本的树和树的基本的遍历办法,图完全不会
    6单片机基础:大约两年的单片机基础
    7新手看到这里,可能已经慌乱了。。。。不要怕,我说的很多东西,写嵌入式系统用不到啊 ,继续,发一个精简版

    精简版
    1:c语言 能把我推荐的c书的第一本或者c primer之类的书看个一半,或者自己本身知道指针的概念,知道结构体指针,函数指针,二级指针的用法,仅仅是概念和写法就行了,不用深入,用不了多久。
    2汇编:知道有几条常用指令,用法和功能是什么就可以了
    3组成原理:知道中断是什么,入栈和出,寄存器,知道汇编对应的计算机大概动作就可以了,用不了一个周
    4操作系统:找个公开课或者书,看看大概的os概念,一个周就够了,我现在很多概念还是不知道,后面可以慢慢学
    5数据结构:会写链表,队列就行了,我们不写文件系统,不用树
    6单片机基础:用过单片机,不用自己写驱动代码,仅仅可以在别人的驱动下,编写一些简单的逻辑代码就行,完全没学过的人,两个礼拜就可以用stm32来编程了,如果之前学过51,一个礼拜不到即可,因为我们只是借助一下单片机,避免使用虚拟机,方便操作系统的调试。因为我们可以用单片机,输出某些信息在串口或者液晶屏,让我们直接看到代码的错误点,方便我们调试。

    因为很多大一的新生想写,推出一个极限版~~
    极限版
    1.学过C,知道有指针这个东西,其他的边做边学。
    2.不懂汇编,边做边查,边查边写。
    3.知道什么是寄存器,知道中断的概念
    4.知道OS是由什么组成的
    5.数据结构完全不会,遇到链表,队列时再查,或者直接抄也行
    6.不学如何使用单片机,遇到再查


    发一个很装逼的封面助兴



    正文:

    一、开发环境

    对我来言,这倒是很重要的一点。第一次萌生想写OS的想法时,在网上搜索了不少资源,大多数都是在叙述框架,如何构建一个操作系统。然而对于当时的我来说,根本不知道用什么平台来写,如何调试自己的程序,看了一些朋友的发帖,推荐用XX模拟器,在XX平台开发,完全看不懂,界面好像也很老旧,而且大多是英文,当时有点敬而远之。自己手上只有个codeblocks软件,想来想去也不知道怎么用这个开发OS。直到有一天,突然顿悟,OS不就是一堆程序,我还打算让他运行在单片机上,那么用单片机常用的开发工具不就行了!!!---------Keil uVision5

    学过单片机的朋友,相信非常熟悉这个软件,使用起来也非常简单,网上随便一查,就可以下载和安装了,这里就不详细展开说了。如果不知道如何使用的,先熟悉一下这个环境,再开始写,相信半个小时即可上手。


     

    二、参考资料

    既然是运行在真机上,必然要对它有所了解,我们这里采用的是STM32(Cortex-M3架构),市面上非常火热的一款,资料丰富,大家有什么问题谷歌一下,有很多前人的经验让你借鉴。如果不知道如何谷歌的朋友,点开这个链接去操作,免费,大约15分钟就能翻墙了如何优雅的访问谷歌、谷歌学术等网站 | 欧拉的博客

    1.Cortex-M3权威指南(中文版),这本书会详细的讲解,中断处理,异常,ARM汇编等知识,我们会在任务切换的时候用到。(PDF即可)

     

    2.嵌入式实时操作系统ucos(邵贝贝审校),ucos中有很多我们可以借鉴的地方。

     

    3.谷歌,有一些基础知识遗忘的时候,谷歌可以让你很快补充上来

     


     




     

    三、从写一个最简单的os做起

    我们这里假设我们写一个支持32个任务并发执行的简易OS

    1.任务就绪表

    我们假设这里任务有两种状态,就绪态和非就绪态。

    我们定义一个32位的变量OSRdyTbl(就绪表),它的最高位(第31位)为最低优先级,最低位(第0位)为最高优先级。OSSetPrioRdy()函数的功能是,你传递一个数(优先级),把这个优先级对应的任务设置为就绪态。同理,见图:

    OSDelPrioRdy()函数将某任务从就绪表移除

    OSGetHighRdy()函数选出最高优先级的任务

    这里我们就完成了,设置某任务为就绪态,从就绪态删除某任务,获得最高优先级任务的任务。

    2.任务控制块

    想必这个概念大家很清楚了,每个任务都对应一个任务控制块,典型的用处是,任务切换时,通过控制块来获知每个任务的堆栈(因为控制块有指针指向该任务的堆栈)

    此外,再定义几个变量。

    注释写的很清楚,不解释了!

    3.主堆栈

    在此STM32中,提供两个堆栈指针,一个是主堆栈(MSP),一个是任务堆栈(PSP),可以通过查Cortex-M3权威指南(中文版)得到。所以我们既要建立一个主堆栈,又要为每个任务建立自己的堆栈(一个任务一个),这里我们先不管任务堆栈,只看主堆栈。

    OS_EXCEPT_STK_SIZE是个宏,大家可以自己设定,我这里设的是1024,一定要尽量大一些。为什么?因为裸机下,进入中断服务程序时,系统会把许多寄存器入栈,而且支持中断嵌套,也就是刚入栈完又入栈,所以有可能会堆栈溢出,非常危险。

    CPU_ExceptStkBase大家先别管,它指向的是数组最后一个元素。

    4.建立一个任务

    我们通过Task_Create()函数来建立一个任务,第一个参数用来传递该任务的任务控制块,第二个参数用来传递函数指针,第三个传递该任务的堆栈。tcb->StkPtr=p_stk这句将该任务控制块中应当指向栈顶的指针,指向了该任务的新栈顶(前面定义了TCB,自己可以翻一翻),在写该函数时,一定要看Cortex-M3权威指南,不然你怎么知道有这么多寄存器,而不是仅仅从R1到R7?看到这里,还想看懂的,应该都是真的想写OS的朋友,这里有不懂的去看Cortex-M3权威指南,你会豁然开朗的。这里,Task_End是一个几乎永远不会执行的函数,何时会执行,先不管了,看它的内容。

    5.欢迎来到主函数

    ①第一行:在该主函数中,第一行我们让主堆栈指针指向了主堆栈,那么,这个主堆栈是哪里来的呢?很简单,我们自己定义的,哈哈。

    很熟悉吧,前面发过这个图,大小你来指定,注意要大!!!!!

    ②第二、三行:建立一个任务,第一个参数传递了该任务的控制块,第二个参数是该任务的任务函数,第三个是堆栈(数组最后一个)

    是不是很好奇,任务1和任务2是什么?一起了来看

    大家自己随意定义,开心就好。

    Task_Switch()函数又是什么呀?

    这里Task_Switch()是我们用来测试的程序,当任务1运行时,完成i++后,将最高优先级设置为任务2,并用OSCtxSw()切换到任务2,OSCtxSw是用汇编写的,是一个隐藏BOOS,我们先不管。

    ③第四行:程序刚运行时,是没有最高优先级的,所以我们用p_TCBHighRdy=&TCB_Task1;来随意指定一个任务为最高优先级

    ④:最后一行:OSStartHighRdy()该函数也是汇编,和OSCtxSw()并称为2大BOSS,我们会在后面解密。OSStartHighRdy和OSCtxSw很相似,不过OSStartHighRdy()用于当所有任务都没有运行过时,用于初始化(当然具有任务切换的作用)并成功运行第一个任务,而OSCtxSw()是在OSStartHighRdy()之后,使用OSCtxSw()时,最起码有一个任务已经运行了(或正在运行)。

    6.完工?

    到这里,从宏观说已经基本完工了,这就是一个简易的OS的基本状况。看到这里,大家可以休息一下了,消化一下,马上有BOSS要出现了,解决那两个BOSS后,就真正做成了一个简易的OS。

     

    7.两大boss之OSStartHighRdy()函数

    7.1任务视图

    终于开始了任务切换环节,这是我画的一副任务切换图,自我感觉非常好,不过大家应该看起来很困难,字太丑了~~~~

    通过分析上面这张图,来确定如何写OSStartHighRdy()函数:

    ①中:此时有一个任务1,但是任务1我们仅仅是建立了,并没有让它运行。这里我们认为任务1是由三部分组成:任务代码,任务堆栈,该任务的任务控制块。

    ②中:我们想让任务1运行起来,任务1是什么?就是一堆代码,如何运行起来?-----让PC(程序计数器)指向任务代码即可(依靠出栈POP)。同时,我们还要让SP指向任务的堆栈,这里的SP当然是PSP(任务堆栈)

    7.2写OSStartHighRdy()函数

    下面开始写OSStartHighRdy(),它的功能就是上图的①和②

    2,3,4,5行中的那些数字,是外设对应的地址,我们往相应的地址写值,即可完成某些目的。12,14,15行是我们之前定义过的几个变量,忘了的回头翻一翻。17行是将OSStartHighRdy()函数extern了一下,因为我们在主函数要用。

    上图就是OSStartHighRdy的内容,我们一起来看。28,29,30行设置了PendSv异常的优先级,问题来了,什么是PendSv???

    Cortex-M3权威指南中其实讲了,很详细,这里为了缩减篇幅,不详细说,大家只用知道PendSV 异常会自动延迟任务切换的请求,直到其它的中断处理程序都完成了处理后才放行。而我们只用触发PendSV异常,并把任务切换的那些步骤,写在PendSv中断处理任务中。

    7.2.1PendSv处理程序

    经过39和40行,我们往控制器里写值,触发了PendSv异常,现在程序会进入异常处理程序,就是下图:

    在此函数中,如果PSP为0,则进入OS_CPU_PendSVHandler_nosave()函数,其实在OSStartHighRdy我们将PSP设置为了0,所以必然会进入OS_CPU_PendSVHandler_nosave()函数。

    7.2.2OS_CPU_PendSVHandler_nosave()函数

    在该函数中,我们让p_TCB_Cur指向了最高优先级的任务,因为有出栈,所以重新更新了SP。

    7.2.3恭喜

    到了此处,一个任务已经可以成功运行起来了!!!!!!

    BUT only one task!我们需要让它多任务切换

    7.3写任务切换OSCtxSw()函数

    与OSStartHighRdy非常相似,也是往中断控制器里写值,进入PendSv异常。

    7.3.1任务视图

    依然是这张喜闻乐见的图!!!!!!!

    ③:任务1要切换到任务2,因为待会要进入任务2,会破坏任务1,所以我们要保存任务1的现场,把寄存器入栈

    ④:因为任务1有入栈动作,栈顶肯定变了,我们修改任务1控制块的值,让它指向新的栈顶

    ⑤:什么都没有,只是想说明。我们建立了一个任务2,仅仅是建立了。

    ⑥:很简单,要想让任务2运行,则让PC和SP分别指向任务2的任务代码和任务堆栈即可。

    ⑦:什么都没有,只是想说明,任务2活的很开心,可以运行了

    7.3.2由OSCtxSw()进入PendSv异常

    与OSStartHighRdy不同的是,这时的PSP肯定不为0了,所以不会跳转,会顺序执行55行以后的程序,也就是任务视图里的③,继续执行④。执行完60行的程序后,继续顺序执行,执行下面程序:

    和OSStartHighRdy函数的后续步骤几乎一样,找到优先级最高的任务,让指针指向它即可!!!!

    1.大功告成!!!!!!!!

    到这里,我们已经彻底完成了一个简易OS的设计!!!!

    8.1如何查看成果?

    8.1.1增添程序

    我们在主函数中加入一个函数

    大家可以把它理解为一个库,调用之后,我们就可以在串口(屏幕)显示某些字符了。

    同理,在任务1和任务2中加入printf函数

    8.1.2编译并下载程序

    8.1.3利用串口调试助手观察

    网上搜串口调试助手,会有很多工具,随意下一个就OK!

    可以看到,按照我们的预期,任务1和任务2轮流输出字符在屏幕上!

    8.2如何调试程序?

    8.2.1调试器

    必然是神器--J-link或者ST-link,一个大概50,嵌入式开发神器,记得以前刚开始玩单片机的时候,调试全靠打印消息在屏幕,觉得好用的不得了,经常有人给我说,你用调试器调试啊,我都鄙夷的回一句,需要么?(哈哈,那时确实不需要) 等开始写OS时才知道,这东西真是救命稻草,没有它,怎么看寄存器的值和异常返回的值呢?

    8.2.2界面

    点击DEBUG后,你可以看到寄存器的值。想想我们之前要入栈,出栈,如果哪一步错了,自己估计把串口吃了,也看不出来吧,哈哈!!!!!!

    单步调试什么的,不说了,用的很多。

     

    9.写到此处的感想

    答主因为写这个OS,在凳子上久坐了两周,这两天腰疼,只好躺着写这个回答了!哈哈!

    也正好因为腰疼,感觉时间比较多。不过和想象的真的不一样,本来觉得可以一气呵成,结果短短的篇幅,就写的自己的思维都大乱了,而且也挺费时间的,前后用了有6个小时了。想写OS的朋友,参考上面的步骤,加上自己琢磨,应该也能写一个出来。如果哪里有不清楚的,不要心急,如果真的是一两天就写成了,还有什么锻炼的意义,有点失去初衷了。不懂的就去查权威指南和OS的书籍,相信你会收获的非常多!




    为什么要写这个回答?
    这是我写了6个小时多以后来补充的问题,因为我自己也纳闷了,放着自己的事不做,跑来写这么一堆干什么........刚才路上走着想到答案了----------想留个纪念。还有不到两个月就要寒假了,我打算考研,也就是说这是大学里做的最后一个项目了(毕设除外),真的挺伤感。从大一一路折腾过来,现在要突然一年不能折腾,简直泪流满面!!!!!!!!!!!!!!!!!!希望我这个简单的开头,能让想写OS的新人得到一丝启发。

    写操作系统能学到什么?
    这也是我想写这个回答的原因,对我的改变挺大
    前几天有个好朋友(大一开始和我一起学单片机的朋友,后来他一直在做单片机项目,却没有补过任何基础知识),打电话问我XX模块怎么用,其实是很简单的一个问题,直接百度都可以,但是他还是想要来问下我,我说很简单,就XX做就可以,有问题你再百度,但是他好像不想听到这种回答,想让我说到他一听就知道怎么做了,才敢开始做,不然就不敢启动项目。那时我才突然意识到,以前的我就是这样啊,做什么项目做什么东西,老是想要集成的模块,资料丰富的模块,如果没有什么源驱动代码,我就不敢做了,生怕买回来,用不了之类的,甚至有源码也不敢买,因为源码和我的机型不一样,连修改源码都怕。开始写OS,我还蛮恐惧的,因为不是科班,没学过,不懂。涉及的东西也很广,从编程语言到数据结构,组成原理,操作系统,怕拿不下来,也找不到好的资源,不知道怎么写起。通过这次项目,我想应该学到了一下东西:

    1.遇到不懂的没有那种很强的抵触感了,开始学会查芯片手册,查原理书,开始配合谷歌查BUG,现在即使是拿来我没有接触过的模块,无论最后做的出来与否,我肯定先去了解它是什么,概念是什么,再去找厂家提供的资料,提供的源码,去一行一行的看,自己修改或者重写,这个比下面说到的什么具体的知识,我想都重要的多。我现在还记得第一次和第二次,第三次打开Cortex-M3权威指南(中文版)时的情景,看了几页就吓得我关了,简直是天书啊,其实耐心看,真的能看懂。
    2.把自己数据结构里学的东西,真正带到了项目,虽然也写过队列等这些数据结构的题,但是在以前的嵌入式开发里,几乎用不到,有时候觉得好没用啊这些。这次通过实现OS的消息队列,看linux的文件系统,知道了这些在实际的巨大用处。
    3.对OS的基本概念和运转有了认识
    4.对C语言的理解深刻了许多
    5.每个人的体验都不一样,没什么补充的了O(∩_∩)O哈哈~
     

    四、简单几步将简易OS改造为--优雅的简易OS

    1.为什么不优雅?

    在该函数中,任务1执行完,立马就会切换到任务2,然而在实际中,我们希望是这样的:

    任务1每100毫秒执行一次

    2.系统节拍

    几乎每个实时操作系统都需要一个周期性的时钟源,称为时钟节拍或者系统节拍,用来跟踪任务延时和任务等待延时

    我们在main函数中输入这样一句

    这里我们配置了定时器中断,每5毫秒一次中断。中断后会进入中断处理函数,下面来写中断处理函数:

    大家只用管if里面的函数即可:我们在某个函数中将TCB_Task[i].Dly置为x,中断处理函数会每5毫秒,将非0的TCB_Task[i].Dly减一。如果TCB_Task[i].Dly非0,对应的任务是不会运行的(因为被我们删除就绪态了,这里看不到),当TCB_Task[i].Dly减为0,我们才将该任务置为就绪态

    3.编写OSTimeDly()函数

    也就是1中图片所示的函数,它可以让某任务指定每X毫秒运行一次。

    第65行,可以关闭中断,同理,第68行,开启中断。为什么要关闭中断?因为中断会影响我们执行下面的代码,先关闭中断,执行完后再打开。66行将该任务从就绪态变为非就绪态,将要延迟的时间赋值为TCB_Task[OSPrioCur].Dly,然后调度(也就是切换任务)

    4.调度函数OSSched()

    非常简单,刚才我们将某个任务变为了非就绪态,紧接着就找就绪态任务中优先级最高的任务,然后切换

    5.是否完成了呢?----空闲任务

    乍一看,好像完成了,实则不然,虽然我们任务1每X毫秒运行一次,任务2每Y毫秒运行一次,但终归里面有空闲时间:任务1和任务2都不在运行,所以我们需要创建一个空闲任务,当CPU没有东西可以运行时,运行空闲任务。以下就是空闲任务:

    6.来到主函数:

    其他的倒是没问题,72行有个陌生的函数:OS_TASK_Init();

    其实就是之前的OSStartHighRdy()函数的升级版,非常简单

    先创建一个空闲函数,再获取优先级最高的任务,然后执行最高优先级的任务。

    7.一个优雅的简易OS诞生了

    不好看?想加个界面?没问题-----其实已经有了,大家观察58行,就是让液晶屏显示一句话,我们把背景修改为红色,醒目一点:



    很开心能有这么多朋友喜欢,非常感谢 。我开了一个简单的头,相信真的喜欢os的朋友,只要认真去做,一定也能实现一个更好的作品。后面的暂时不打算写,如果有了新的思路,一定会再写出来。授人以鱼不如授人以渔,看到这里已经有动手的能力了,想写的朋友不要害怕,尽管去做!!!加油


    五、加入信号量
    六、加入消息队列
    七、内存管理
    八、实现一个free和malloc玩玩?

    编辑于 2016-12-05

    ​赞同 492​​50 条评论

    ​分享

    ​收藏​感谢收起​

    logo中国婚博会

    广告​

    不感兴趣知乎广告介绍

    中国婚博会(上海站)限量门票免费领取中!

    2019年8月24-25日中国婚博会(上海站)世博展览馆开展!荟萃婚庆、摄影、珠宝、婚品、婚车...2天帮您搞定婚礼,门票免费快递到家!查看详情

    fleuria

    fleuria

    刀剑的时代已经过去,接下来的战斗要拿起枪

    246 人赞同了该回答

    也可以参考我的毕业论文: 基于 Bochs 的操作系统内核实现

    汇编不重要,但是要有一定计算机组成的基础,并对一个现代 kernel 的结构有大体的认识,至少大致上理解虚拟内存和文件系统有哪些东西。不要看 《the orange's》和《三十天编操作系统》,面太小,代码质量不高,就别拿 DOS 当操作系统了。个人比较推荐《莱昂氏 UNIX 源码分析》(已绝版,可淘宝打印)、《linux 0.11 内核详解/剖析》 ,写代码之前至少先都啃一遍。教程的话推荐 《bran's kernel development tutorial》和 osdev 上的一些资料。顺着它们搭开发环境,出一个简单的 bootlaoder,可以编译 C 代码即可。然后拿大把大把的时间慢慢给 bootloader 加东西就好了,能用 C 就不要用汇编。开发中的很多细节要到开发时才留意的到,这时可以自己思考,也可以去抄 linux 0.11, xv6, unixv6 这几套优秀的源码。

    现在想来,开发一个 kernel 的主要内容在于“实现”而不是“设计”,更重要的是用时间去理解这些优秀的设计为什么合理,自己别出心裁的想法一般不用多想,一定是错的。

    不要在 x86 的一些历史遗留问题上花太多时间,比如 bootloader 的保护模式会在开头挡住一大批人,可是这并不重要,只要知道有这个接口可以引导你的二进制代码即可。知道 GDT 可以区分用户态/内核态,IDT 可以给中断绑回调就行了。在调试上会花费大量时间,可以慢慢琢磨怎样提高调试的效率。然后需要的就只是耐性了,说实话也挺无聊。

    编辑于 2018-03-02

    ​赞同 246​​30 条评论

    ​分享

    ​收藏​感谢

    周仕成

    周仕成

    uiuc cs专业

    33 人赞同了该回答

    我来骗一下star哈哈
    szhou42/osdev
    虚拟内存✔
    硬盘驱动,EXT2文件系统,虚拟文件系统✔
    简陋的GUI界面✔
    多进程(烂尾楼 :) )✔
    网络数据收发(只是简单收发raw packets,各种网络协议还在写)✔




    这是我学习过程中用到的主要网站和资料

    BrokenThorn Entertainment

    Global Descriptor Table

    Unofficial Mirror for JamesM's Kernel Development Tutorial Series

    Expanded Main Page


    Build software better, together :)

    楼上大神全是几千字长文看起来有点怕,我简明扼要地说一些写OS的重要心得吧

    1 教程上没看懂的代码千万不要照抄,最好在自己理解的基础上重新写一遍。我在看JamesM's Kernel教程的paging部分时就吃了这个亏,全盘抄了他的代码,结果发现他的代码总有神奇的bugs让你的os崩溃。结果我把写了一个半月的代码全部推翻,在完全理解后自己重新写出来的paging代码,不能说完全没bugs吧,至少现在已经四五个月了都没有再因为paging部分的代码而使os崩溃。

    2 搭建方便使用的调试器,我个人用的是qemu+gdb配合,源码级调试

    3 在os最基础的设施(中断,异常,VGA driver)都实现后,马上写个printf和hexdump函数,因为有一些极端情况,gdb下断点+单步跟踪+观察变量 这种办法会失效 :(。

    4 快点写个malloc函数!越快越好!!文件系统,进程管理,GUI这些都需要用到大量的数据结构,而最方便的方法就是用malloc来申请和释放这些结构。

    最后,说一下os开发的流程,这只是我个人的路线。
    第一步,很多人会想写bootloader,但是我建议先跳过这一步,直接用grub或者qemu的自带bootloader,先跳过这些繁琐的细节,专注于OS内核的开发。

    第二步,建立好各种gdt,idt,中断,异常等机制,这样系统出什么错的时候马上就能发现。

    第三步,printf函数,这意味着你得先写VGA driver,但两者都不难

    第四步,实现虚拟内存和分页机制,在此基础上实现kmalloc函数。

    第五步,实现多进程/线程

    第六步,写个PCI驱动!PCI是用来访问各种硬件的,例如硬盘,网卡,都得通过PCI来控制,实际上我就是因为想写硬盘驱动,才写的PCI驱动。

    第七步,写硬盘驱动,实现EXT2文件系统,实现VFS文件系统。

    第八步,GUI,设计一种数据结构存储和显示各种窗口。初步可以用VGA试验一下编写图形操作系统的乐趣,但是要想有高分辨率 真彩色还是得写VESA驱动才能得到。 很多人觉得图形操作系统很酷炫,但这反而是写OS里面最简单,最容易调试的一步(当然了要做VESA驱动还是很麻烦,因为在保护模式下没法用中断调用)。

    第九步,实现网卡驱动,实现TCP/IP协议栈!

    第十步,发挥你的想象!Network File System maybe?

    编辑于 2017-01-05

    ​赞同 33​​2 条评论

    ​分享

    ​收藏​感谢收起​

    frank yao

    frank yao

    虎妈猫爸

    19 人赞同了该回答

    我大约是在08-09年的时候写过一个迷你的操作系统。大家可以在这里看到我的源代码 https://code.google.com/p/minios2/。当时这个项目完全是自己的兴趣爱好。后来代码较多了觉得需要花费过多的精力不合适就放弃了。

     

    整个项目是从0开始的。因为主要在windows上开发,所以主力编译器是msvc6.0。虽然说很不可思议。但是当你明白了编译链接的原理以及PE文件的格式之后,这其实并不难。当然现在如果用高版本的msvc写的话会更容易。

     

    另一个难点是需要寻找以及阅读大量的资料。包括比如386保护模式,bios调用,8259中断控制器,pci总线控制器,8253时钟控制器,ATA硬盘控制器,各种以太网卡控制器等。当时这些资料在网上非常分散,收集很不容易。现在貌似好找多了。

     

    另外你需要对数据结构,计算机体系结构以及操作系统原理有一定的了解。这个基本上本科和研究生课程里的知识就足够了。当然你也要有足够的编程经验,因为有些错误可能会很难调试。


     

    以下是我当时写的一个简单的文档。

    minios目前已经完成的功能:

    bootsector

    进入保护模式

    内存分配模块

    简单的线程调度模块

    信号量

    时钟

    延时调用DPC和定时调用TPC

    统一的设备驱动模型

    标准输入输出设备驱动

    内存及字符串相关的标准C库函数

    附件codes.zip的目录结构如下:

    codes

    |-relocate 连接程序的源代码,将bootsector和minios连接成一个可启动的磁盘镜像

    |-bootsector bootsector的源代码

    |-minios minios的源代码

    |-bin 所有的目标都在此目录中。其中minios.vhd就是可启动的磁盘镜像

    如何启动minios:

    你必须安装Microsoft的Virtual PC 2007

    你可以在微软的官方网站下载他的安装程序,程序大小约30M

    http://download.microsoft.com/download/8/5/6/856bfc39-fa48-4315-a2b3-e6697a54ca88/32%20BIT/setup.exe

    安装完成后就可以双击codes/bin/vm.vmc运行minios了

    如何编译minios:

    编译minios共需要三种编译器。

    codes/bootsector/bootsector.asm必须用nasm进行编译,将编译的结果命名为bootsector并且拷贝到codes/bin

    codes/minios/platform/platform.asm必须用masm32进行编译,编译的结果在codes/minios/platform/platform.obj

    其余的代码都用vc6编译即可,vc6的工程在codes/minios/minios.dsw

    如果你手边没有nasm和masm32,不要紧,因为这两个文件一般不需要改动,直接用我编译好的目标文件就可以了

    双击minios.dsw打开vc6,点击菜单Project->Project Setting->Debug,修改Executable for debug session一栏

    将Virtual PC.exe的完整路径填入。如果你安装在默认的路径下,就不需要修改它。

    然后直接Ctrl-F5运行就可以编译并且运行了。

     

    vc工程是在dll的工程的基础上配置的

    1、将所有相关的文件加到工程中来。

    2、由于对于debug版本的代码生成,vc会加入不少调试代码,不好控制,所以删除Win32 Debug的配置

    3、由于默认的Release配置中,会加入Intrinsic Functions的优化,他会用vc libc中的函数代替你写的标准C语言库函数。因此必须自定义优化方案。project setting->C++->Optimizations选customize并且勾上除了Assume No Aliasing, Generate Intrinsic Functions, Favor Small Code, Full Optimization外的优化选项。

    4、在project setting->C++->Preprocessor->Additional include directories中加入include这个目录,并且勾上Ignore standard include paths

    5、project setting->Link中,output file name改成../bin/minios.dll。勾上Ignore all default libraries和Generate mapfile, object/libraty modules中的内容清空

    6、project setting->Link->Debug中mapfile name改成../bin/minios.map,project setting->Link->Output中Entry-point symbol改成main

    7、project setting->post-build step中添加一行"../bin/relocate.exe" ../bin/minios.dll ../bin/bootsector ../bin/minios.vhd

    8、project setting->Debug中Executable for debug session改成C:\Program Files\Microsoft Virtual PC\Virtual PC.exe,working directory改成../bin,Program arguments改成-singlepc -pc vm -launch

    如果我没有忘记什么的话,应该就这些了。这样你的vc就可以编译minios的原代码了。编译的结果在../bin/minios.dll

    为什么使用dll的工程呢?

    因为windows的dll中有一个relocation的段,他列出了该dll文件如果要重定位的话所有需要修改的地址偏移。假设dll默认的加载位置是0x10000000,而在minios中我希望把它定位在0x400000则只需要把重定位表的每一项所指向的地址减去(0x10000000- 0x400000)就可以了。这也是relocate.exe这个程序的主要工作。

    至于具体pe文件的结构以及重定向表的结构,网上有很多,我手边暂时没有资料,可以参看relocate.exe的原代码

    minios的引导过程和内存布局

    首先,pc机的bios程序会将bootsector加载到0x7C00, 此时段寄存器的值我也不大清楚,但是不要紧,自己重新设一遍吧。把ds, es, ss都设成cs一样的值,把sp放在0x8000的位置上,这样我们就有了512字节的堆栈了。

    然后,bootsector将minios读出放在从0x400000开始的内存空间上。随后bootsector简单的设置了GDT后直接进入保护模式并且将控制权交给minios的entrypoint。从0x100000到0x3fffff是内核堆,内核所需要的动态内存都可以从此堆上使用keMalloc和keFree分配。内存最低的4K字节被用来存放中断向量指针,就像纯DOS那样。从0x4000开始到0x8000存放了PCI总线配置数据块。从0x10000到0x1ffff的64K字节用来作为IDE的DMA内存块,其他的低端内存暂时还没有分配,可能会作为文件系统的缓存。

    main函数先重新配置了8259中断控制器,8253时钟控制器,设置了IDT GDT,初始化时钟,内核堆和任务子系统后,建立了第一个任务main,入口是keEntryMain

    keEntryMain首先打开中断并且加载console和keboard的驱动,然后建立DPC任务,kdebug任务以及一个测试任务

    编辑于 2015-11-11

    ​赞同 19​​1 条评论

    ​分享

    ​收藏​感谢收起​

    Pandaos

    Pandaos

    不会数学的信安菜狗,立志转行生物信息学,三本肄业

    15 人赞同了该回答

    (年代久远凭记忆写,有错误请指出)

    已经过去五年了,现在想起来仍然觉得热血沸腾,致那个中二的年纪~~ ~我已经很久没碰代码了,现在很迷茫,偶然看到这个话题,才回想起来我曾经也是一个有梦想的少年。

     

    我曾经学习8086汇编的时候,有幸认识了一个开发OS的朋友,他为我打开了一个新世界的大门。他建立了一个QQ群里来交流OS开发心得。。开发OS的有两类人,一类是喜欢技术喜欢折腾的,另外一类是喜欢拉帮结派搞计划的。我很反感第二类,个人写的OS顶多就是用来学习底层知识的,上不了台面,有些人非觉得自己很牛逼要搞点大事情出来。我是一个比较保守的人,很长一段时间对这些写OS的人嗤之以鼻,我觉得他们写的就是一个裸机程序,有的代码量还没我用易语言写的QQ机器人的代码多,竟然自称OS?

     

    我在开发OS之前翻了翻《Windows 内核情景分析》。当时还在学习Windows内核驱动,这本书可以学习ReactOS(仿WinNT内核)各方面的实现,比如系统调用,内存分配等等。书分为上下两册,很厚,其实我看得云里雾里的。还看过Linux内核源码剖析方面的书籍。我那个时候看过很多书,但是都很晕。可贵的是我有心思去看,去思考。现在再让我看这些书籍,我心里不太愿意,长大了,开始变得功利起来。

     

    入门一般从引导程序开始,引导程序存储在mbr,加电后,cpu默认处于实模式。实模式下的汇编主要可以参考王爽的汇编教材。一般mbr引导程序只有几百个字节大小,所以需要完成进一步加载,加载一个更大的引导程序。 初始化过程中需要把CPU切到保护模式,保护模式的书籍可以参考李忠的《href="http://www.baidu.com/link?url=hRMBwBQb7-jpSQFzdNVo1HCEb4szWKlw6-duNjU1WF_yb2qs0noVLcGOlnMebUoWkZgCKOlbAsOLZwbOWUQ46_">x86汇编语言 从实模式到保护模式完整版》 。如果还想专业一点可以参考《x86/x64体系探索及编程 (邓志)》或者intel的官方手册。《30天自制操作系统》我个人不太喜欢,感觉没有oranges专业。

     

    引导程序都是用汇编写的,即使一个再简单的内核也需要很多代码,全用汇编不太现实。引导程序需要想办法加载用高级语言编写的程序。现在的编译器目标二进制比较常见的是elf和pe,这就需要进一步学习elf或pe文件结构知识。建议选elf,装载器有源码参考。解析文件格式可能很繁琐,也可以直接将elf或pe中的bin脱出来用或者自己设计文件格式,但是要考虑重定位等问题。

     

    我折腾OS的时候,基础数理知识极度欠缺,比如画图只会画直线,于是放弃了UI开发路线,转向命令行模式。内存分配,各种乱七八糟的算法都搞不懂,于是自己意淫了一种方法出来,碎片?不存在的,反正目前阶段内存是用不完的。。。后来,学pwn的时候认真阅读了glibc的malloc实现,才感觉到曾经的方法是多么弱智。

    我写OS之前写过一段时间win驱动,对windbg极度依赖,寻思着能不能用易语言搞一个类似的调试器,折腾一段时间后我放弃了。串口通信搞定了,搞不定如何单步,各种莫名奇妙的错误。根本原因还是OS的很多基本工作没有做完就想着做调试体系,而且没有任何一本书讲过这个。后来干脆模仿windows蓝屏,把错误信息打在屏幕上。

    文件系统。我对实用性有着很高的追求,自己实现简易文件系统虽然来得快(不健全有文件系统的样子),但是不实用。于是选择先折腾fat32,这又是一个巨坑。后来,我发现网传的硬盘读写方法巨坑,只有ide能用,这是我放弃开发的原因之一。

     

    总之,写os不要觉得自己在搞大事,我们都是学习者。

    编辑于 2019-05-04

    ​赞同 15​​2 条评论

    ​分享

    ​收藏​感谢收起​

    林瑟

    林瑟

    嘟嘟嘟嘟嘟嘟嘟

    17 人赞同了该回答

    照片删掉了,大家专心看技术吧

    终于!!!一个混大数据圈的我也能有朝一日来回答这个话题了。请在座的各位多多包涵,下面我就要开始我的表演了!!!

    先把我的原文地址安排在这里,请大家走过路过不要错过,随手点击支持一下我这个可爱的女程序员吧(你支持我我支持我导师:):

    操作系统基础: C 语言实现用户态线程(实战)​gitbook.cn图标

     

    ——————一本正经的美颜分割线————————

    之前我上大学老师讲的时候大概说过这个操作系统的知识,那会只知道是最底层的语言,了解原理活学活用就行,之后开始工作了也没用得上,就渐渐都忘干净了,直到有一天重新抹着泪捡起来,这个过程也挺有意思的呢。

    我从零开始捡起的时候,主要是学习了线程切换和角度,看了大量的操作系统的书,还有 Linux 的源码甚至反汇编了 Windows 的内核代码吧啦吧啦的,反正最后没看懂!!(简直闻着伤心听者落泪呜呜呜)

    爆哭

    书和源码都太抽象了,学起来超级困难的呀,但是因为第一份工作,要懂那些用户态线程的基本知识,但我这方面的基本功还不扎实,那段日子我都瘦了(想想还是很难过...)后来我听公司的前辈们说 Golang 的 Goroutine,鹅场开源的 libco,狼场 BRPC 中的 bhtread 都离不开这个,我也只好硬着头皮去搞了。

     

    好的各位哥哥前辈们,我的硬核知识来了,先安排一下我的实验环境(突然正经:)

    • ubuntu 16.04 32 位操作系统(最好是提前安装好哦);
    • 挑选一个你觉得好用的虚拟机软件,比如 VMWare;
    • 把虚拟机环境配置成单核 CPU。

    呵 这么复杂的内容,想不到吧,我自己还搞了四五个小时呢!!

    但是你肯定要问了,这都 9012 年了为啥还在用 32 位的系统尼??!

    毕竟是初学者,请各位大佬谅解谅解哈,我也是为了快速掌握原理嘛

    最后的效果图就是这样滴~~

    oh 对了 我用 C 语言搞出来了,果然我大学老师说得没错, C 简直是万能的啊,只有想不到,没有做不到!!

    想给大家提醒的是,上面的代码,并没有使用操作系统为我们提供的pthread系列函数,thread_createthread_join函数都是自己纯手工实现的。唯一使用操作系统的函数就是设置时钟,因此会有时钟信号产生,这一步是为了模拟时间片轮转算法而做的。

    贴一段我上面动图的 demo 示例代码:

    这篇文章预计是个长文,所以可能不会一下子更新完毕。。。。。

    所以先把我要说的一些点列出:

    • 控制流切换原理
    • 上下文切换
    • 线程设计
    • 调度函数的封装与代码模块化
    • 线程的主动切换
    • 时间片轮转调度

    小姐妹叫我吃饭了哈哈哈哈哈哈回来再更吧哈哈哈哈哈哈

    恰饭!!

    ——————第二天的分割线——————

    哦果然,没人注意到我这个小透明,那我可以放心叭叭叭了。这篇教程来自于我在公司的导师,简直拯救我于水火中的大佬,所以来炫耀一下成果哈哈哈哈哈

    早起更新,先来说一下控制流切换原理

    控制流,指的是一系列按顺序执行的指令。多控制流,是指存在两个或两个以上可以并发(宏观同时,微观不同时)执行的指令序列。比如你编写的多线程程序,每个线程就可以看成是一个控制流,多个线程允许多个控制流一起执行。

    在我们学习编程的时候,如果不借助操作系统提供的线程框架,几乎无法完成多控制流的运行的。

    接下来先来剖析一下,我们的指令如何”莫名奇妙“的就切换到其它线程的。

    1.1 指令执行

    不管你用的是什么语言编程,最后都要落实到 CPU 上,而 CPU 只认识它自己的语言,机器语言。机器语言可以抽象出对应 CPU 架构的汇编指令。如下面的 x86 指令序列。

    程序在执行时,实际上就是汇编指令(准确的说是机器指令)在 CPU 上一条一条执行。对于单核 CPU 来说,永远只有一条控制流,也就是只有一条指令序列。所以,宏观上模拟的多线程程序,本质上还只是单控制流,所谓的多线程,只不过是一种被制造出来的假像!

    注:有部分同学没有接触过汇编指令,不要害怕,我们用到的汇编不会太难!

    1.2 控制流切换(x86 CPU 架构)

    汇编指令在执行的时候,最重要地方在于它需要依赖 CPU 环境:

    • 一套通用寄存器 (eax、edx、ecx、ebx、esp、ebp、esi、edi);
    • 标志寄存器 eflags;
    • 指令寄存器 eip (eip 用来保存下一条要被指令的地址)。

    如果你不理解 CPU 寄存器是什么意思,把它想象成它是 CPU 中预先定义好的变量。也不知道大家有没有看懂唉,我觉得我导师这里讲的还是蛮清楚的啊啊啊啊啊啊真的佩服了!!大佬就是我学习的榜样啊喂!

    操作系统基础: C 语言实现用户态线程(实战)​gitbook.cn图标

    如果各位有更好的方法在评论区给我留言讨论一下哦,不然我就默默更贴了。。。。

    还有一个很重要环境,就是。因为指令序列在执行时,经常会保存一些临时数据,比如某条指令的地址。当指令执行 ret 指令的时候,CPU 会从当前栈顶弹出一个值到 eip 寄存器!这意味着要发生跳转了!

    通用寄存器中,有一个寄存器名为 esp,它保存的是栈顶指针(内存地址的值)。指令 push 、 pop、call、ret 都依赖于 esp 工作。

    • call 指令把它后面的指令地址保存到 esp 指向的内存单元,同时修改 eip。如 call 0x2000,先把 call 0x2000 的下一条指令地址压栈,同时修改 eip 为 0x2000。
    • ret 指令把 esp 指向的内存单元中的值保存到 eip。
    • push 指令把值保存到 esp 指向的内存单元。
    • pop 把 esp 指向的内存单元的值取出。

     

    图2 CPU 寄存器 esp 与内存单元的关系,右侧表示运行栈

    想象一下,如果某个时候,我们把 esp 保存的数据 “偷偷” 换了,换句话说我们是把栈换了,而栈中保存的那个“某条指令”的地址的值也不一样了,将会发生什么?图3 把 esp 的值从 0xFFE8 更改成了 0x1FFE8。

     

    图3 切换 esp 即切换栈

    所谓的切换控制流,无非就是更改 esp 栈顶指针来做到偷梁换柱的把戏而已。只不过,为了做到惟妙惟肖,我们在更改 esp 的时候,还得顺带的把通用寄存器环境修改修改,以适应新的那段“某条指令”的执行环境。(注意,栈中经常会保存某条指令的地址,比如函数的返回地址。)

    通常,这段新的“某条指令”的执行环境,恰好也保存在栈里,就像上图中 esp 到“某条指令地址”之间那段内存的数据(五颜六色的那部分数据)。

    说了这么多,其实也很抽象对不对,大家可以去看我导师举的栗子,还是很一目了然的,我就不在这里贴出来了,为什么呢。因为他写的文章太长太长又太详细,需要去专心的读,再加上动手操作试试,我当初看到这篇指导教程的时候作为徒弟内心有一万个不服,我就不信自己一个外行的看完了就能搞明白???????

    然而事实证明,我打脸了,我导师的这篇文,真的很强,不会是狼场的码农,真的够狠!!!剩下的文章,大家点上面的原文链接去看吧,我可能有点佛系不会按时更新(说白了就是忙。。。)

    好嘞,我们下次更新时再见吧!!请走过路过的小哥哥小姐姐们给我点个赞吧,谢谢哦~

    编辑于 2019-05-08

    ​赞同 17​​8 条评论

    ​分享

    ​收藏​感谢收起​

    我现在只想搞钱

    我现在只想搞钱

    23 人赞同了该回答

    这个问题码了得有一两年了。。终于算是有资格来回答这个问题。


    先上代码

    项目地址:MRNIU/SimpleKernel

    这是我正在写的内核,目的是尽量让刚刚接触的人可以基于已有代码拓展出自己的内核来。

    • 基本信息

    语言:C/C++, shell, x86 ATT Assembly

    启动:GRUB2

    Kernel Model:宏内核

    虚拟机:bochs

    支持平台:Linux/Mac

    • 进度

    已完成:

    1. GDT、IDT 的基本设置
    2. 键盘输入
    3. libc 中的部分函数
    4. 中断体系的框架

    TODO:

    1. 内存管理
    2. debug
    3. 进程
    4. 图形界面
    5. POSIX API
    6. 设备管理
    7. 网络
    8. etc.

    目前正在将启动支持迁移到 multiboot2,顺便把内存管理和 debug 的部分前置内容做了。

    这是目录结构

    ├── bochsout.txt bochs 输出
    ├── bochsrc.txt bochs 配置文件
    ├── setup.sh
    ├── simplekernel.img
    ├── someknowledge
    ├── src/ 源码目录
    │   ├── Makefile 构建规则
    │   ├── arch/ 架构相关代码
    │   │   ├── README.md
    │   │   ├── i386/ i386 架构
    │   │   │   ├── README.md
    │   │   │   ├── boot/ 启动代码,使用 multiboot
    │   │   │   │   ├── boot.s
    │   │   │   │   └── link.ld
    │   │   │   ├── clock.c 时钟
    │   │   │   ├── clock.h
    │   │   │   ├── cpu.hpp CPU操作
    │   │   │   ├── debug/ 调试函数
    │   │   │   │   └── debug.c
    │   │   │   ├── intr/ 中断设置
    │   │   │   │   ├── README.md
    │   │   │   │   ├── intr.c idt 设置
    │   │   │   │   ├── intr.h
    │   │   │   │   └── intr_s.s
    │   │   │   └── mm/ 内存管理
    │   │   │       ├── README.md
    │   │   │       ├── gdt.c gdt 设置
    │   │   │       ├── gdt.h
    │   │   │       ├── gdt_s.s
    │   │   │       ├── pmm.c 物理内存管理
    │   │   │       ├── pmm.h
    │   │   │       ├── vmm.c 虚拟内存管理
    │   │   │       └── vmm.h
    │   │   └── x64/ x64 架构
    │   │       └── TODO
    │   ├── include/ 头文件
    │   │   ├── DataStructure/ 可能会用到的数据结构与算法
    │   │   │   ├── BinarySearchTree.cpp 二叉树
    │   │   │   ├── DataStructuer.h
    │   │   │   ├── LinkedList.cpp 链表
    │   │   │   ├── Queue.cpp 队列
    │   │   │   ├── SortAlgorithm.cpp 排序算法
    │   │   │   └── Stack.cpp 栈
    │   │   ├── README.md
    │   │   ├── debug.h
    │   │   ├── drv/ 设备头文件
    │   │   │   ├── keyboard.h
    │   │   │   └── mouse.h
    │   │   ├── elf.h elf 格式定义
    │   │   ├── kernel.h 内核函数直接引用的头文件
    │   │   ├── libc/ c 标准库
    │   │   │   ├── README.md
    │   │   │   ├── assert.h 断言
    │   │   │   ├── stdarg.h
    │   │   │   ├── stdbool.h
    │   │   │   ├── stddef.h
    │   │   │   ├── stdint.h
    │   │   │   ├── stdio/ 标准输入输出
    │   │   │   │   ├── printk.c
    │   │   │   │   └── vsprintf.c
    │   │   │   ├── stdio.h
    │   │   │   ├── string/ 字符串处理
    │   │   │   │   └── string.c
    │   │   │   ├── string.h
    │   │   ├── mm/ 内存相关头文件
    │   │   │   ├── README.md
    │   │   │   └── mm.h
    │   │   ├── multiboot.h 多重引导规范定义
    │   │   ├── pic.hpp 8259A 中断芯片设置
    │   │   ├── port.hpp 端口操作
    │   │   ├── tty.hpp tty 定义
    │   │   └── vga.hpp vga 显示定义
    │   └── kernel/
    │       ├── README.md
    │       ├── drv/ 设备
    │       │   ├── kb.c
    │       │   └── mouse.c
    │       └── kernel.c 内核入口
    └── tools/ 工具,在 .rb 文件中你可以找到 gcc 和 binutils 的编译选项
        ├── i386-elf-binutils.rb
        └── i386-elf-gcc.rb
    
    
    

    欢迎讨论

    发布于 2019-01-07

    ​赞同 23​​2 条评论

    ​分享

    ​收藏​感谢收起​

    Coldwings

    Coldwings

    Python 话题的优秀回答者

    6 人赞同了该回答

    操作系统这玩意…并不是都像windows那样图形界面一堆工具,甚至不像linux发行版那样带一堆命令行工具。
    以linux为例,图形界面就不说了命令行?那是bash,是个独立软件包,人家在bsd在unix在darwin上都跑得妥妥的。
    一个纯粹的操作系统,其实只是定义了驱动接口(用别人的驱动),定义了最简单的进程调度管理,定义了内存分配。这就已经是操作系统了。
    所以写一个新的操作系统真的真的不是特别困难。困难的是你的os出来之后除了你自己大概是不会有人给他写驱动写程序的,除非用户多;啥都没有的os不会有人用。于是恶性循环…

    发布于 2015-02-08

    ​赞同 6​​1 条评论

    ​分享

    ​收藏​感谢

    lizhimeng159

    lizhimeng159

    知易行难/INTJ

    13 人赞同了该回答

    寒假开始有个想法,突然想写一个最简单的操作系统,基于单片机的。

    于是找人借来学校索奥社团的板子,历时四天,写了个atmega16的os内核,能实现,任务调度,任务内嵌信号量,任务延迟,具体效果如下

     

    大概就是这样的
    任务建立如下如图

    三个任务

    流水灯,蜂鸣器,数码管。

    之后再更具体制作过程和所需要求。

    (突然发现不能放视频....哪一张图片那闪几个灯的自动忽略吧,本来想发一下跑任务的视频的)

    一,关于所需要的(储备)知识

    1,微机原理

    emmmm,用到的有关微机原理的东西就是 汇编

    能用到的指令大概就是 POP 和 PUSH这种级别的..

    然后需要知道 堆栈保存现场的原理

    2,单片机原理及其应用

    主要是,这个内核是基于单片机的,还是基于AVR单片机,所以就不说哪些高大上的linux什么东西。

    中断和定时器得知道怎么回事吧。

    主要是知道SP(堆栈指针),PC(程序计数器)是怎么回事,并且在什么时候(主动或者被动)修改他们

    3,C语言

    这里面用到的C语言也就有下面几个东西

    指针(如函数指针等),struct,typedef ,与或运算,for循环,while等等,

    有点编程基础都会。

    4,单片机RTOS

    这个,我觉得会不会无所谓吧,说到底这个内核是超简单的内核,没用什么算法,遍历什么的都是for循环....简而言之,就是,简单易懂原理。

    二,原理概述

    不如我打个比方。

    假设人A在一个屋子里,屋子里有三个房间,分别是卧室,客厅,厨房,主人要在一上午的时间里完成如下事情。

    事情一:

    卧室很乱,需要人打扫整理。

    事情二:

    电话在客厅里,有可能别人来打电话谈事情。

    事情三:

    厨房正在烧水,到水烧开后,主任A要去厨房断点拿水壶。

    论重要性,则有水烧开后拿水壶>接电话>收拾卧室。

    场景:

    1,A在收拾卧室,此时电话响了,A停止收拾,卧室现场被保存(卧室不变)

    2,A去客厅接电话讨论事情,在接电话过程中,水烧开了,水壶给出信号,A给对方说,先停止这个讨论,讨论内容此时被记录(下次结着这次内容继续讨论)

    3,A去厨房处理水壶,处理完成







     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 有人说,世间一切,都是...因为CSDN,我更珍惜每一博友、每一朋友、每一老师,解答大家的问题,鼓励考研或找工作失败的人继续战斗;因为CSDN,我认识了女神,并分享了许多我们一家的故事。感恩遇见,负青春。

    从2010年我来到CSDN,再到2013年我撰写第一篇博客,转眼已经过去十年。590篇原创文章,786万次阅读量,19万位关注博友,这一个个数字的背后,是我3000多天的默默付出,也是我写下近千万文字的心血。

    有人说,世间一切,都是遇见,都是机缘。是啊,因为CSDN,我与很多人成为了好朋友,虽未谋面,但这种默默鼓励、相互分享的感觉真好;因为CSDN,我人生进度条八分之一(十年)的许多故事在这里书写,笔耕不辍,也算不得辜负时光吧;因为CSDN,我更珍惜每一位博友、每一位朋友、每一位老师,解答大家的问题,鼓励考研或找工作失败的人继续战斗;因为CSDN,我认识了女神,并分享了许多我们一家的故事。

    东西湖的夜很静,博士的征途很辛苦,远方的亲人异常思念。
    为什么我要写这样一篇文章呢?一方面,感谢读者这十年的陪伴和包容,不论我分享什么内容,你们给予我的都是鼓励和感动;另一方面,因为改变,接下来我将短暂告别CSDN一段时间(技术更新放缓),好好沉下心来去读论文,去做做科研。

    同时,这篇文章非常硬核,会利用Python文本挖掘详细分享这十年的故事,也算是为文本挖掘初学者和写相关论文的读者一些福利。真诚的对所有人说一声感谢,感恩这十年的陪伴,不负遇见,不负时光。请记住一个叫Eastmount的分享者,此生足矣~

    在这里插入图片描述


    一.忆往昔分享岁月

    关于作者与CSDN这十年的故事,可以阅读这篇文章:

    十年,转瞬即逝,我从青葱少年成长为了中年大叔。或许,对其他人来说写博客很平淡,但对我来说,它可能是我这十年最重要的决定和坚守之一。

    十年,不负遇见,不负自己,不负时光。感恩所有人的陪伴,因为有你们,人生路上我不孤单。幸好,这十年来,我可以摸着自己的良心说,每一篇博客我都在很认真的撰写,雕琢,都在用万字长文书写下我的满腔热血。

    下图是我这十年分享博客按月统计的数量,从2015年找工作分享一次高峰,到如今读博,从零学习安全知识并分享又是一座高峰。

    在这里插入图片描述

    下图是这十年我在CSDN撰写博客的主题演化图,整个十年,我经历了四个阶段。

    • 本科阶段:2013年3月至2014年8月
      当时以本科课程为主,包括C#、网络开发、课程基础知识等等。
    • 硕士阶段:2014年9月至2016年8月
      该阶段研究生方向为NLP和知识图谱,因此撰写了大量的Python基础知识,包括Android、C#、面试和LeetCode、网站开发等等。
    • 工作阶段:2016年9月至2019年7月
      该阶段作者初入职场,选择回到贵州当一名普通的大学教师,并分享了《Python数据挖掘》《网站开发》等课程,撰写《Python人工智能》《Python图像处理》等专栏。
    • 博士阶段:2019年9月至2021年4月
      该阶段作者再次返回校园,离别家乡亲人选择读博,并换了大方向,转而学习系统安全和网络安全,大量安全知识从零学起,《网络安全自学篇》《网络安全提高班》《系统安全和恶意代码检测》专栏也开启。

    在这里插入图片描述

    有许多人问我,“你分享快乐吗?”
    快乐。其实每写一篇博客我的内心都非常喜悦的,每看到读者的一个点赞或评论,我真的开心得像一个小孩。

    那为什么还要短暂消失呢?
    因为毕业,因为想家,因为想他(她)。我相信,大多数分享者都和我有同样的心情,分享知识的魅力让人久久不能忘怀。但每个阶段都需要做每个阶段的事,远方的亲人尤为思恋,经过我反复思考,所以我决定短暂放下技术博客的撰写,转而选择论文研究。

    在这里插入图片描述

    短暂的消失,并不意味着不分享。
    而接下来90%的分享都将与论文和科研技术相关,并且每个月不再PUSH自己写文。我不知道接下来的几年,我究竟能做到什么程度,我也不能保证能否发一篇高质量的论文,但我会去拼搏,会去战斗,会去享受。况且,这十年走来,我从来不认为自己是个聪明的人,比我优秀的人太多太多,我更喜欢的是默默撰写,默默经历,陪着大家一起成长。别人称赞我博客的时候,我更多的回复是“都是时间熬出来的”,也确实是时间熬出来的,只是写了3012天。

    但我是真的享受啊,享受在CSDN所分享的一切,享受与每一位博友的相遇相识,享受每一位朋友的祝福与鼓励,我感恩的写下590篇文章,65个专栏,千万文字和代码,也勉强可以说上一句“不负遇见,不负青春,此生足矣”。

    在这里插入图片描述

    在这里插入图片描述

    下图展示了这十年我写的博客涉及的各个方向。这些年,我一直都知道学得太杂,而没有深入,就希望博士期间能深入某个领域,博一博二安全基础知识也学了很多,所以接下来是时候进入第五个阶段,开启论文的阅读和撰写以及实验复现。也希望博友们理解,更期待您的陪伴。

    在这里插入图片描述

    沙子是握不住的,时间也是。
    但当我付出之后,我可以随手把它扬起,我可以把在时间中发生的点滴记录,比如技术、又如爱情。读博苦吗?苦,无数个寂静的夜都需要我们去熬,去拼,但有的人更苦,比如家里的另一位。接下来三年,我希望自己始终记住,我为什么选择来到这里,选择来到东西湖。也是时候沉下心来去学习论文和做实验了,技术分享该放就放,虽然不舍。握不住的沙,就随手扬了它;即便回到原点,我也没有失去什么,况且这段经历也是人生的谈资啊。也希望每一位博友都珍惜当下,都去做自己喜欢的事情,去经历。

    在这里插入图片描述

    我看着路,梦的入口有点窄,这或许是最美丽的意外。
    这篇文章我将使用在CSDN的第一次群发,还请见谅,下一次应该是2024年我博士毕业那天。再次感谢所有人的陪伴,一个好的分享者需要不断去学习新知识,前沿技术再总结给大家,所以我们应该尊重每一位创作者的果实。同时,我在这里向所有读者保证,三年之后,我将带着新的理解,新的感受,去分享更优质的文章,去回馈所有读者,帮助更多初学者入门,或许手痒我也会写一篇非常详细的总结吧。

    再次感谢大家,希望大家记住CSDN有一位叫Eastmount的作者,一位叫杨秀璋的博主,如果能记住娜璋和小珞一家就更开心了,哈哈~爱你们喔,困惑或遇到困难的读者,可以加我微信共同前行。

    在这里插入图片描述

    我们的故事都还在续写,你们的陪伴依然继续。
    最后,熟悉我的读者知道我开了三个付费专栏。常常有读者因在校读书或经济拮据,因此我在文中多次提到可以私聊我给全文,其实我早已把这些文章开源到了github,我更希望每一位读者都能从文章中学到知识,希望觉得文章好且手里轻松的给个9块打赏,奶粉钱就够了。在此,我也把这三个地址分享给需要的读者吧!且行且珍惜,购买也欢迎喔。

    • Python图像处理
      https://github.com/eastmountyxz/CSDNBlog-ImageProcessing
    • 网络安全自学篇
      https://github.com/eastmountyxz/CSDNBlog-Security-Based
    • Python人工智能
      https://github.com/eastmountyxz/CSDNBlog-AI-for-Python

    在这里插入图片描述

    给想学技术的说声抱歉,大家记得等我喔!江湖再见,感恩同行。

    在这里插入图片描述



    二.硬核-CSDN博客文本挖掘

    之前我给学安全的读者一波福利,告诉大家安全学习路线及CSDN优秀的博主。

    这里,我最后再给Python文本挖掘读者一波福利。希望您喜欢~这篇文章思路大家可以借鉴,但不要直接拿去写论文喔!但思路已经非常清晰,大家一定动手撰写代码。

    1.数据爬取

    这里不介绍具体代码,保护CSDN原创,但会给出对应的核心知识点。建议读者结合自己的方向去抓取文本知识。

    核心扩展包:

    • import requests
    • from lxml import etree
    • import csv

    核心流程:

    • 解决headers问题
    • 解决翻页问题
    • 审查元素分析DOM树结构
    • 定位节点采用Xpath分析
    • 分别赚取标题、URL、时间、阅读和评论数量
    • 详情页面抓取

    在这里插入图片描述

    在这里插入图片描述

    爬虫输出结果,建议学会打桩输出(多用print)。

    在这里插入图片描述

    整理后的结果如下图所示,内容输出到CSV存储。

    在这里插入图片描述


    2.计量统计和可视化分析

    (1) 按小时分析作者撰写习惯
    首先,我们来分析作者“Eastmount”的撰写博客习惯,同时利用Matplotlib和PyEcharts绘制图形,发现Echarts绘制的图形更好看。由图可知,该作者长期在深夜和下午撰写博客。

    在这里插入图片描述

    源代码如下:

    # encoding:utf-8
    """
    By:Easmount CSDN 2021-04-19
    """
    import re
    import time
    import csv
    import pandas as pd
    import numpy as np
    
    #------------------------------------------------------------------------------
    #第一步 读取数据
    dd = []   #日期
    tt = []   #时间
    with open("data.csv", "r", encoding="utf8") as csvfile:
        csv_reader = csv.reader(csvfile)
        k = 0
        for row in csv_reader:
            if k==0:      #跳过标题
                k = k + 1
                continue
            #获取数据 2021-04-08 21:52:21
            value_date = row[4]
            value_time = row[5]
            hour = value_time.split(":")[0]
            hour = int(hour)
            dd.append(row[4])
            tt.append(hour)
            #print(row[4],row[5])
            #print(hour)
            k = k + 1
    print(len(tt),len(dd))
    print(dd)
    print(tt)
    
    #------------------------------------------------------------------------------
    #第二步 统计不同小时的个数
    from collections import Counter
    cnt = Counter(tt)
    print(cnt.items())  #dict_items
    #字典按键排序
    list_time = []
    list_tnum = []
    for i in sorted(cnt):
        print(i,cnt[i])
        list_time.append(i)
        list_tnum.append(cnt[i])
    
    #------------------------------------------------------------------------------
    #第三步 绘制柱状图
    import matplotlib.pyplot as plt
    N = 24
    ind = np.arange(N)
    width=0.35
    plt.bar(ind, list_tnum, width, color='r', label='hour') 
    plt.xticks(ind+width/2, list_time, rotation=40) 
    plt.title("The Eastmount's blog is distributed by the hour")  
    plt.xlabel('hour')  
    plt.ylabel('numbers')  
    plt.savefig('Eastmount-01.png',dpi=400)  
    plt.show()
    
    #------------------------------------------------------------------------------
    #第四步 PyEcharts绘制柱状图
    
    from pyecharts import options as opts
    from pyecharts.charts import Bar
    bar=(
        Bar()
            .add_xaxis(list_time)
            .add_yaxis("数量", list_tnum, color="blue")
            .set_global_opts(title_opts=opts.TitleOpts(
                title="Eastmount博客按小时分布", subtitle="hour"))
        )
    bar.render('01-Eastmount博客按小时分布.html')
    

    (2) 按月份统计博客
    作者按月份撰写博客如下图所示,2015年找工作撰写了大量LeetCode代码,后续是读博期间安全分享较多。

    在这里插入图片描述

    源代码如下:

    # encoding:utf-8
    """
    By:Easmount CSDN 2021-04-19
    """
    import re
    import time
    import csv
    import pandas as pd
    import numpy as np
    
    #------------------------------------------------------------------------------
    #第一步 读取数据
    dd = []   #日期
    tt = []   #时间
    with open("data.csv", "r", encoding="utf8") as csvfile:
        csv_reader = csv.reader(csvfile)
        k = 0
        for row in csv_reader:
            if k==0:      #跳过标题
                k = k + 1
                continue
            #获取数据 2021-04-08 21:52:21
            value_date = row[4]
            value_time = row[5]
            hour = value_time.split(":")[0]   #获取小时
            hour = int(hour)
            month = value_date[:7]            #获取月份
            dd.append(month)
            tt.append(hour)
            #print(row[4],row[5])
            #print(hour,month)
            print(month)
            k = k + 1
            #break
    print(len(tt),len(dd))
    print(dd)
    print(tt)
    
    #------------------------------------------------------------------------------
    #第二步 统计不同日期的个数
    from collections import Counter
    cnt = Counter(dd)
    print(cnt.items())  #dict_items
    #字典按键排序
    list_date = []
    list_dnum = []
    for i in sorted(cnt):
        print(i,cnt[i])
        list_date.append(i)
        list_dnum.append(cnt[i])
    
    #------------------------------------------------------------------------------
    #第三步 PyEcharts绘制柱状图
    from pyecharts import options as opts
    from pyecharts.charts import Bar
    from pyecharts.charts import Line
    from pyecharts.commons.utils import JsCode
    
    line = (
        Line()
        .add_xaxis(list_date)
        .add_yaxis('数量', list_dnum, is_smooth=True,
                   markline_opts=opts.MarkLineOpts(data=[opts.MarkLineItem(type_="average")]),
                   markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="max"),
                                                           opts.MarkPointItem(type_="min")]))
        # 隐藏数字 设置面积
        .set_series_opts(
            areastyle_opts=opts.AreaStyleOpts(opacity=0.5),
            label_opts=opts.LabelOpts(is_show=False))
        # 设置x轴标签旋转角度
        .set_global_opts(xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-30)), 
                         yaxis_opts=opts.AxisOpts(name='数量', min_=3), 
                         title_opts=opts.TitleOpts(title='Eastmount博客按日期分布'))        
        )
    line.render('02-Eastmount博客按日期分布.html')
    

    (3) 按星期统计博客
    按星期统计如下,调用date.weekday()函数可以输出对应的星期。周末作者更新稍微少一些。

    在这里插入图片描述

    核心代码如下:

    # encoding:utf-8
    """
    By:Easmount CSDN 2021-04-19
    """
    import re
    import time
    import csv
    import pandas as pd
    import numpy as np
    import datetime
    
    #定义星期函数
    def get_week_day(date):
        week_day_dict = {
            0 : '星期一',
            1 : '星期二',
            2 : '星期三',
            3 : '星期四',
            4 : '星期五',
            5 : '星期六',
            6 : '星期天'
        }
        day = date.weekday()
        return week_day_dict[day]
    
    #------------------------------------------------------------------------------
    #第一步 读取数据
    
    dd = []   #日期
    tt = []   #时间
    ww = []   #星期
    with open("data.csv", "r", encoding="utf8") as csvfile:
        csv_reader = csv.reader(csvfile)
        k = 0
        for row in csv_reader:
            if k==0:      #跳过标题
                k = k + 1
                continue
            #获取数据 2021-04-08 21:52:21
            value_date = row[4]
            value_time = row[5]
            hour = value_time.split(":")[0]   #获取小时
            hour = int(hour)
            month = value_date[:7]            #获取月份
            dd.append(month)
            tt.append(hour)
    
            #获取星期
            date = datetime.datetime.strptime(value_date, '%Y-%m-%d').date()
            week = get_week_day(date)
            ww.append(week)
            #print(date,week)
            k = k + 1
    print(len(tt),len(dd),len(ww))
    print(dd)
    print(tt)
    print(ww)
    
    #------------------------------------------------------------------------------
    #第二步 统计不同日期的个数
    from collections import Counter
    cnt = Counter(ww)
    print(cnt.items())  #dict_items
    #字典按键排序
    list_date = ['星期一','星期二','星期三','星期四','星期五','星期六','星期天']
    list_dnum = [0,0,0,0,0,0,0]
    
    for key,value in cnt.items():
        k = 0
        while k<len(list_date):
            if key==list_date[k]:
                list_dnum[k] = value
                break
            k = k + 1
    print(list_date,list_dnum)
    
    #------------------------------------------------------------------------------
    #第三步 PyEcharts绘制柱状图
    from pyecharts import options as opts
    from pyecharts.charts import Bar
    from pyecharts.charts import Line
    from pyecharts.commons.utils import JsCode
    bar=(
        Bar()
            .add_xaxis(list_date)
            .add_yaxis("数量", list_dnum, color='pink')
            .set_global_opts(title_opts=opts.TitleOpts(
                title="Eastmount博客按星期分布", subtitle="week"))
        )
    bar.render('03-Eastmount博客按星期分布.html')
    

    3.核心词统计及词云分析

    词云分析非常适合初学者,这里作者也简单分享核心主题词统计和词云分析的过程。

    (1) 统计核心关键词及词频
    输出结果如下图所示:

    在这里插入图片描述

    代码如下:

    # coding=utf-8
    """
    By:Easmount CSDN 2021-04-19
    """
    import jieba
    import re
    import time
    import csv
    from collections import Counter
    
    #------------------------------------中文分词----------------------------------
    cut_words = ""
    all_words = ""
    stopwords = ["[", "]", ")", "(", ")", "(", "【", "】",
                 ".", "、", "-", "—", ":", ":", "《", "》",
                 "的", "和", "之", "及", "“", "”", "?", "?"]
    
    #导入自定义词典
    #jieba.load_userdict("dict.txt")
    
    f = open('06-data-fenci.txt', 'w')
    
    with open("data.csv", "r", encoding="utf8") as csvfile:
        csv_reader = csv.reader(csvfile)
        k = 0
        for row in csv_reader:
            if k==0:      #跳过标题
                k = k + 1
                continue
            #获取数据
            title = row[1]
            title = title.strip('\n')
            #print(title)
            #分词
            cut_words = ""
            seg_list = jieba.cut(title,cut_all=False)
            for seg in seg_list:
                if seg not in stopwords:
                    cut_words += seg + " "
            #cut_words = (" ".join(seg_list))
            f.write(cut_words+"\n")
            all_words += cut_words
            k = k + 1
    f.close()
    #输出结果
    all_words = all_words.split()
    print(all_words)
    
    #------------------------------------词频统计---------------------------------- 
    c = Counter()
    for x in all_words:
        if len(x)>1 and x != '\r\n':
            c[x] += 1
    #输出词频最高的前10个词
    print('\n词频统计结果:')
    for (k,v) in c.most_common(10):
        print("%s:%d"%(k,v))
    #存储数据
    name ="06-data-word.csv"
    fw = open(name, 'w', encoding='utf-8')
    i = 1
    for (k,v) in c.most_common(len(c)):
        fw.write(str(i)+','+str(k)+','+str(v)+'\n')
        i = i + 1
    else:
        print("Over write file!")
        fw.close()
    

    (2) PyEcharts词云可视化
    输出结果如下图所示,出现词频越高的单词显示越大、越鲜艳。

    在这里插入图片描述

    代码如下:

    # coding=utf-8
    """
    By:Easmount CSDN 2021-04-19
    """
    import jieba
    import re
    import time
    import csv
    from collections import Counter
    
    #------------------------------------中文分词----------------------------------
    cut_words = ""
    all_words = ""
    stopwords = ["[", "]", ")", "(", ")", "(", "【", "】",
                 "01", "02", "03", "04", "05", "06", "07",
                 "08", "09", "什么"]
    f = open('06-data-fenci.txt', 'w')
    
    with open("data.csv", "r", encoding="utf8") as csvfile:
        csv_reader = csv.reader(csvfile)
        k = 0
        for row in csv_reader:
            if k==0:      #跳过标题
                k = k + 1
                continue
            #获取数据
            title = row[1]
            title = title.strip('\n')
            #print(title)
            #分词
            cut_words = ""
            seg_list = jieba.cut(title,cut_all=False)
            for seg in seg_list:
                if seg not in stopwords:
                    cut_words += seg + " "
            #cut_words = (" ".join(seg_list))
            f.write(cut_words+"\n")
            all_words += cut_words
            k = k + 1
    f.close()
    #输出结果
    all_words = all_words.split()
    print(all_words)
    
    #------------------------------------词频统计---------------------------------- 
    c = Counter()
    for x in all_words:
        if len(x)>1 and x != '\r\n':
            c[x] += 1
    #输出词频最高的前10个词
    print('\n词频统计结果:')
    for (k,v) in c.most_common(10):
        print("%s:%d"%(k,v))
    #存储数据
    name ="06-data-word.csv"
    fw = open(name, 'w', encoding='utf-8')
    i = 1
    for (k,v) in c.most_common(len(c)):
        fw.write(str(i)+','+str(k)+','+str(v)+'\n')
        i = i + 1
    else:
        print("Over write file!")
        fw.close()
    
    #------------------------------------词云分析----------------------------------
    from pyecharts import options as opts
    from pyecharts.charts import WordCloud
    from pyecharts.globals import SymbolType
    
    # 生成数据 word = [('A',10), ('B',9), ('C',8)] 列表+Tuple
    words = []
    for (k,v) in c.most_common(200):
        # print(k, v)
        words.append((k,v))
    # 渲染图
    def wordcloud_base() -> WordCloud:
        c = (
            WordCloud()
            .add("", words, word_size_range=[20, 40], shape='diamond') #shape=SymbolType.ROUND_RECT
            .set_global_opts(title_opts=opts.TitleOpts(title='Eastmount十年博客词云图'))
        )
        return c
    
    # 生成图
    wordcloud_base().render('05-Eastmount十年博客词云图.html')
    

    4.LDA主题挖掘

    LDA模型是文本挖掘或主题挖掘中非常经典的算法,读者可以阅读作者之前的文章,详细介绍该模型。这里,我们用它来对作者博客进行主题挖掘,设置的主题数为4,通常需要计算困惑度比较。

    在这里插入图片描述

    同时计算各个主题对应的主题词,如下所示。注意,建议读者结合自己的文本进行相应的优化,这会得到更符合真实情况的主题词,并且主题之间会存在相互交融的现象,比如安全系列博客,会有Python相关的渗透文章。

    在这里插入图片描述

    完整代码如下:

    #coding: utf-8
    import pandas as pd
    from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
    
    #---------------------  第一步 读取数据(已分词)  ----------------------
    corpus = []
    
    # 读取预料 一行预料为一个文档
    for line in open('06-data-fenci.txt', 'r').readlines():
        corpus.append(line.strip())
            
    #-----------------------  第二步 计算TF-IDF值  ----------------------- 
    # 设置特征数
    n_features = 2000
    tf_vectorizer = TfidfVectorizer(strip_accents = 'unicode',
                                    max_features=n_features,
                                    stop_words=['的','或','等','是','有','之','与','可以','还是','这里',
                                                '一个','和','也','被','吗','于','中','最','但是','大家',
                                                '一下','几天','200','还有','一看','300','50','哈哈哈哈',
                                                 '“','”','。',',','?','、',';','怎么','本来','发现',
                                                 'and','in','of','the','我们','一直','真的','18','一次',
                                               '了','有些','已经','不是','这么','一一','一天','这个','这种',
                                               '一种','位于','之一','天空','没有','很多','有点','什么','五个',
                                               '特别'],
                                    max_df = 0.99,
                                    min_df = 0.002) #去除文档内出现几率过大或过小的词汇
    tf = tf_vectorizer.fit_transform(corpus)
    print(tf.shape)
    print(tf)
    
    #-------------------------  第三步 LDA分析  ------------------------ 
    from sklearn.decomposition import LatentDirichletAllocation
    # 设置主题数
    n_topics = 4
    lda = LatentDirichletAllocation(n_components=n_topics,
                                    max_iter=100,
                                    learning_method='online',
                                    learning_offset=50,
                                    random_state=0)
    lda.fit(tf)
    # 显示主题数 model.topic_word_
    print(lda.components_)
    # 几个主题就是几行 多少个关键词就是几列 
    print(lda.components_.shape)                         
    # 计算困惑度
    print(u'困惑度:')
    print(lda.perplexity(tf,sub_sampling = False))
    
    # 主题-关键词分布
    def print_top_words(model, tf_feature_names, n_top_words):
        for topic_idx,topic in enumerate(model.components_):  # lda.component相当于model.topic_word_
            print('Topic #%d:' % topic_idx)
            print(' '.join([tf_feature_names[i] for i in topic.argsort()[:-n_top_words-1:-1]]))
            print("")
    # 定义好函数之后 暂定每个主题输出前20个关键词
    n_top_words = 20                                       
    tf_feature_names = tf_vectorizer.get_feature_names()
    # 调用函数
    print_top_words(lda, tf_feature_names, n_top_words)
    
    #------------------------  第四步 可视化分析  ------------------------- 
    import pyLDAvis
    import pyLDAvis.sklearn
    
    #pyLDAvis.enable_notebook()
    data = pyLDAvis.sklearn.prepare(lda,tf,tf_vectorizer)
    print(data)
    #显示图形
    pyLDAvis.show(data)
    pyLDAvis.save_json(data,' 06-fileobj.html')
    

    5.层次聚类主题树状图

    层次聚类绘制的树状图,也是文本挖掘领域常用的技术,它会将各个领域相关的主题以树状的形式进行显示,这里输出结果如下图所示:

    在这里插入图片描述

    注意,这里作者可以通过设置过滤来显示树状图显示的主题词数量,并进行相关的对比实验,找到最优结果。

    # -*- coding: utf-8 -*-
    import os
    import codecs
    from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
    from sklearn.manifold import TSNE
    from sklearn.cluster import KMeans
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import jieba
    from sklearn import metrics
    from sklearn.metrics import silhouette_score
    from array import array
    from numpy import *
    from pylab import mpl
    from sklearn.metrics.pairwise import cosine_similarity
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    from scipy.cluster.hierarchy import ward, dendrogram
    
    #---------------------------------------加载语料-------------------------------------
    text = open('06-data-fenci.txt').read()
    print(text)
    list1=text.split("\n")
    print(list1)
    print(list1[0])
    print(list1[1])
    mytext_list=list1
    
    #控制显示数量
    count_vec = CountVectorizer(min_df=20, max_df=1000)  #最大值忽略
    xx1 = count_vec.fit_transform(list1).toarray()
    word=count_vec.get_feature_names() 
    print("word feature length: {}".format(len(word)))
    print(word)
    print(xx1)
    print(type(xx1))
    print(xx1.shape)
    print(xx1[0])
    
    #---------------------------------------层次聚类-------------------------------------
    titles = word
    #dist = cosine_similarity(xx1)
    
    mpl.rcParams['font.sans-serif'] = ['SimHei']
    
    df = pd.DataFrame(xx1)
    print(df.corr())
    print(df.corr('spearman'))
    print(df.corr('kendall'))
    dist = df.corr()
    print (dist)
    print(type(dist))
    print(dist.shape)
    
    #define the linkage_matrix using ward clustering pre-computed distances
    linkage_matrix = ward(dist) 
    fig, ax = plt.subplots(figsize=(8, 12)) # set size
    ax = dendrogram(linkage_matrix, orientation="right",
                    p=20, labels=titles, leaf_font_size=12
                    ) #leaf_rotation=90., leaf_font_size=12.
    #show plot with tight layout
    plt.tight_layout() 
    #save figure as ward_clusters
    plt.savefig('07-KH.png', dpi=200)
    plt.show()
    

    6.社交网络分析

    社交网络分析常用于引文分析,文科领域有的成为文献知识图谱(和Google提出的知识图谱或本体有区别),它也是文献挖掘领域常用的技术手段。这里我们绘制社交网络关系图谱如下所示,主要利用Gephi软件,也推荐Neo4j或D3。可以看到作者十年的分享主要集中在四块内容,它们又相互联系,相辅相成。

    • 网络安全
    • Python
    • 逆向分析
    • 基础知识或编程技术

    推荐文章:

    在这里插入图片描述

    第一步,我们需要计算两两共现矩阵。数据量过大可能会边界溢出。
    输出结果如下图所示,此时希望大家进行停用词过滤或将异常关系删除。

    # -*- coding: utf-8 -*-
    """
    @author: eastmount CSDN 2020-04-20
    """
    import pandas as pd
    import numpy as np
    import codecs
    import networkx as nx
    import matplotlib.pyplot as plt
    import csv
    from scipy.sparse import coo_matrix
     
    #---------------------------第一步:读取数据-------------------------------
    word = [] #记录关键词
    f = open("06-data-fenci.txt", encoding='gbk')            
    line = f.readline()           
    while line:
        #print line
        line = line.replace("\n", "") #过滤换行
        line = line.strip('\n') 
        for n in line.split(' '):
            #print n
            if n not in word:
                word.append(n)
        line = f.readline()
    f.close()
    print(len(word)) #关键词总数 2913
    
    #--------------------------第二步 计算共现矩阵----------------------------
    a = np.zeros([2,3])
    print(a)
    
    #共现矩阵
    #word_vector = np.zeros([len(word),len(word)], dtype='float16')
    
    #MemoryError:矩阵过大汇报内存错误 采用coo_matrix函数解决该问题
    print(len(word))
    #类型<type 'numpy.ndarray'>
    word_vector = coo_matrix((len(word),len(word)), dtype=np.int8).toarray() 
    print(word_vector.shape)
    
    f = open("06-data-fenci.txt", encoding='gbk')  
    line = f.readline()           
    while line:
        line = line.replace("\n", "") #过滤换行
        line = line.strip('\n') #过滤换行
        nums = line.split(' ')
    
        #循环遍历关键词所在位置 设置word_vector计数
        i = 0
        j = 0
        while i<len(nums):         #ABCD共现 AB AC AD BC BD CD加1
            j = i + 1
            w1 = nums[i]           #第一个单词
            while j<len(nums):
                w2 = nums[j]       #第二个单词
                #从word数组中找到单词对应的下标
                k = 0
                n1 = 0
                while k<len(word):
                    if w1==word[k]:
                        n1 = k
                        break
                    k = k +1
                #寻找第二个关键字位置
                k = 0
                n2 = 0
                while k<len(word):
                    if w2==word[k]:
                        n2 = k
                        break
                    k = k +1
                #重点: 词频矩阵赋值 只计算上三角
                if n1<=n2:
                    word_vector[n1][n2] = word_vector[n1][n2] + 1
                else:
                    word_vector[n2][n1] = word_vector[n2][n1] + 1
                #print(n1, n2, w1, w2)
                j = j + 1
            i = i + 1
        #读取新内容
        line = f.readline()
        #print("next line")
    f.close()
    print("over computer")
    
    
    #--------------------------第三步  CSV文件写入--------------------------
    c = open("word-word-weight.csv","w", encoding='utf-8', newline='')    #解决空行
    #c.write(codecs.BOM_UTF8)                                 #防止乱码
    writer = csv.writer(c)                                    #写入对象
    writer.writerow(['Word1', 'Word2', 'Weight'])
    
    i = 0
    while i<len(word):
        w1 = word[i]
        j = 0 
        while j<len(word):
            w2 = word[j]
            #判断两个词是否共现 共现词频不为0的写入文件
            if word_vector[i][j]>0:
                #写入文件
                templist = []
                templist.append(w1)
                templist.append(w2)
                templist.append(str(int(word_vector[i][j])))
                #print templist
                writer.writerow(templist)
            j = j + 1
        i = i + 1
    c.close()
    

    第二步,我们需要构建实体(节点)和关系(边)的CSV文件。如下图所示:

    • entity-clean.csv
    • rela-clean.csv

    在这里插入图片描述

    在这里插入图片描述

    第三步,新建工程,并选择“数据资料”,输入电子表格。导入节点表格,选择entity实体表。

    在这里插入图片描述

    第四步,导入数据,设置为“边表格”,注意CSV表格数据一定设置为 Source(起始点)、Target(目标点)、Weight(权重),这个必须和Gephi格式一致,否则导入数据会提示错误。

    在这里插入图片描述

    第五步,导入成功后点击“概览”显示如下所示,接着就是调整参数。

    在这里插入图片描述

    第六步,设置模块化,在右边统计中点击“运行”,设置模块性。同时设置平均路径长度,在右边统计中点击“运行”,设置边概述。

    在这里插入图片描述

    在这里插入图片描述

    第七步,重新设置节点属性。节点大小数值设定为“度”,最小值还是20,最大值还是120。节点颜色数值设定为“Modularity Class”,表示模块化。

    在这里插入图片描述

    第八步,在布局中选择“Fruchterman Reingold”。调整区、重力和速度。

    在这里插入图片描述

    第九步,点击预览。设置宋体字,显示标签,透明度调整为20,如下图所示。

    在这里插入图片描述

    第十步,图谱优化和调整。
    同时可以过滤权重或设置颜色模块浅色。比如得到更为精细的关系图谱。

    在这里插入图片描述


    7.博客情感分析

    情感分析主要采用SnowNLP实验,也推荐大家使用大连理工大学情感词典进行优化。这里推荐作者之前分析的文章。输出结果如下图所示:

    在这里插入图片描述

    但是如果我们计算每天或每月新闻的总体情感分数,就会达到时间序列的情感分析图,从而更好地对情感趋势进行预测,文本挖掘或图书情报领域中使用得也非常多。

    在这里插入图片描述

    # -*- coding: utf-8 -*-
    from snownlp import SnowNLP
    import codecs
    import os
    
    #获取情感分数
    source = open("06-data-fenci.txt", "r", encoding='gbk')
    fw = open("09-result.txt", "w", encoding="gbk")
    line = source.readlines()
    sentimentslist = []
    for i in line:
        s = SnowNLP(i)
        #print(s.sentiments)
        sentimentslist.append(s.sentiments)
    
    #区间转换为[-0.5, 0.5]
    result = []
    i = 0
    while i<len(sentimentslist):
        result.append(sentimentslist[i]-0.5)
        fw.write(str(sentimentslist[i]-0.5)+"\n")
        print(sentimentslist[i]-0.5, line[i].strip("\n"))
        i = i + 1
    fw.close()
    
    #可视化画图
    import matplotlib.pyplot as plt
    import numpy as np
    plt.plot(np.arange(0, 598, 1), result, 'k-')
    plt.xlabel('Number')
    plt.ylabel('Sentiment')
    plt.title('Analysis of Sentiments')
    plt.show()
    

    8.博客主题演化分析

    最后是主题化验研究,这里推荐大家阅读南大核心相关的论文。其实主题演化通常分为:

    • 主题新生
    • 主题消亡
    • 主题融合
    • 主题孤独

    主题融合的计算方法各种各样,大家可以寻找最适合自己论文的方法,比如词频、权重、O系数、关联性分析等等。这里推荐大家使用Echarts绘制,作者的图谱如下图所示:

    在这里插入图片描述

    注意,作者这里给出的代码是另一个案例。但原理一样,仅供参考。真实情况的计算过程更为复杂,计算演化系数通常为小数。

    option = {
        series: {
            type: 'sankey',
            layout:'none',
            focusNodeAdjacency: 'allEdges',
            data: [
            {
                name: 'T1-竞争情报'
            },{
                name: 'T1-企业'
            },{
                name: 'T1-企业管理'
            }, {
                name: 'T1-情报研究'
            },{
                name: 'T1-竞争对手'
            },{
                name: 'T1-情报工作'
            },{
                name: 'T1-市场经济'
            },{
                name: 'T2-竞争情报'
            },{
                name: 'T2-企业'
            },{
                name: 'T2-企业管理'
            },{
                name: 'T2-竞争情报系统'
            },{
                name: 'T2-竞争对手'
            },{
                name: 'T2-知识管理'
            },{
                name: 'T2-反竞争情报'
            },{
                name: 'T3-竞争情报'
            },{
                name: 'T3-企业'
            },{
                name: 'T3-竞争情报系统'
            },{
                name: 'T3-企业管理'
            },{
                name: 'T3-高校图书馆'
            },{
                name: 'T3-反竞争情报'
            },{
                name: 'T3-知识管理'
            },{
                name: 'T4-竞争情报'
            },{
                name: 'T4-企业'
            },{
                name: 'T4-大数据'
            },{
                name: 'T4-产业竞争情报'
            },{
                name: 'T4-竞争情报系统'
            },{
                name: 'T4-高校图书馆'
            },{
                name: 'T4-企业管理'
            }
            
            ],
            links: [{
                source: 'T1-竞争情报',
                target: 'T2-竞争情报',
                value: 10
            }, {
                source: 'T1-企业',
                target: 'T2-企业',
                value: 7
            }, {
                source: 'T1-企业管理',
                target: 'T2-企业管理',
                value: 6
            },{
                source: 'T1-情报研究',
                target: 'T2-竞争情报',
                value: 5
            },{
                source: 'T1-竞争对手',
                target: 'T2-竞争对手',
                value: 5
            },{
                source: 'T1-情报工作',
                target: 'T2-竞争情报',
                value: 3
            },{
                source: 'T1-市场经济',
                target: 'T2-竞争情报',
                value: 3
            },{
                source: 'T1-竞争情报',
                target: 'T2-竞争情报系统',
                value: 5
            },{
                source: 'T1-竞争情报',
                target: 'T2-竞争情报系统',
                value: 3
            },{
                source: 'T1-竞争情报',
                target: 'T2-知识管理',
                value: 3
            },{
                source: 'T1-竞争情报',
                target: 'T2-反竞争情报',
                value: 3
            },
            
            {
                source: 'T2-竞争情报',
                target: 'T3-竞争情报',
                value: 10
            },{
                source: 'T2-企业',
                target: 'T3-企业',
                value: 7
            },{
                source: 'T3-竞争情报系统',
                target: 'T4-竞争情报',
                value: 3
            },{
                source: 'T2-企业管理',
                target: 'T3-企业管理',
                value: 6
            },{
                source: 'T2-竞争情报系统',
                target: 'T3-竞争情报系统',
                value: 5
            },{
                source: 'T2-竞争对手',
                target: 'T3-竞争情报',
                value: 5
            },{
                source: 'T2-知识管理',
                target: 'T3-知识管理',
                value: 3
            },{
                source: 'T2-反竞争情报',
                target: 'T3-反竞争情报',
                value: 3
            },{
                source: 'T2-竞争情报',
                target: 'T3-高校图书馆',
                value: 4
            },
            
            {
                source: 'T3-竞争情报',
                target: 'T4-竞争情报',
                value: 10
            },{
                source: 'T3-企业',
                target: 'T4-企业',
                value: 7
            },{
                source: 'T3-竞争情报',
                target: 'T4-大数据',
                value: 5
            },{
                source: 'T3-竞争情报',
                target: 'T4-产业竞争情报',
                value: 5
            },{
                source: 'T3-竞争情报系统',
                target: 'T4-竞争情报系统',
                value: 6
            },{
                source: 'T3-企业管理',
                target: 'T4-企业管理',
                value: 4
            },
            {
                source: 'T3-高校图书馆',
                target: 'T4-高校图书馆',
                value: 4
            },{
                source: 'T3-反竞争情报',
                target: 'T4-竞争情报',
                value: 3
            },{
                source: 'T3-知识管理',
                target: 'T4-竞争情报',
                value: 2
            }
            
            ]
        }
    };
    

    运行截图如下所示:

    在这里插入图片描述


    9.拓展知识

    读者还可以进行各种各样的文本挖掘,比如:

    • 命名实体识别
    • 知识图谱构建
    • 智能问答处理
    • 舆情事件预测


    三.总结

    最后用我的博客签名结束这篇文章,“无知·乐观·低调·谦逊·生活”,时刻告诉自己:无知的我需要乐观的去求知,低调的底色是谦逊,而谦逊是源于对生活的通透,我们不止有工作、学习、编程,还要学会享受生活,人生何必走得这么匆忙,做几件开心的事,写几篇系统的文,携一位心爱的人,就很好!感恩CSDN,感谢你我的坚守和分享,这又何止是十年。

    感恩所有读者十年的陪伴,短暂消失只为更好的遇见。接下来三年,愿接纳真实的自己,不自卑,不自傲;愿踏踏实实努力、认认真真生活,爱我所爱,无怨无悔,江湖再见。欢迎大家留言喔,共勉~

    在这里插入图片描述

    (By:Eastmount 2021-04-28 晚上12点 http://blog.csdn.net/eastmount/ )


    展开全文
  • 作为一个程序员我为什么博客?

    万次阅读 多人点赞 2016-04-24 14:08:47
    作为一个程序员我为什么博客?从2012-05-22的第一篇博文,到现在累计原创:523篇,转载:182篇,译文:8篇,转载的基本不会占用多少时间,我们来计算一下原创的+译文;这里假设平均一篇博文需要2小时的时间...

    作为一个程序员我为什么要写博客?从2012-05-22的第一篇博文,到现在累计原创:523篇,转载:182篇,译文:8篇,转载的基本不会占用多少时间,我们来计算一下原创的+译文;这里假设平均写一篇博文需要2小时的时间(通常一篇有质量的博文需要一到两天的时间,甚至更长),(523+8)* 2 = 531 * 2 = 1062 小时,如果按工作日计算,一天工作8小时,1062 / 8 = 132,需要132个工作日,换算为人月的话需要将近7个人月来完成。

    这篇博文是在年前就打算写的,但是构思了这么久,期间也看了很多相关的类似的文章,也没想好到底应该怎么写。

    记录这一刻:

    最初的梦想

    这章节标题起的有点扯淡了,不过这首歌挺好听的!

    我为什么要写博客?

    其实我最初打算写博客,第一篇博文的原因很简单,我就是想把工作中、学习中遇到 的问题及解决方案记录下来,方便日后用到的时候拿来就用,不用再去搜索别人的解决方案,一下子得到N多个结果,然后一个一个去验证。

    完了?对,这就完了,原因就这么简单!

    这么简单的一个原因一个理由能让你坚持那么久?不符合常理!

    是什么让我坚持?

    很多人都 是写有什么好处之类的,咱不搞那么高大上,只写一写是什么让我坚持。

    1、自己日后能用到

    确实偶尔会用到,但频率不会那么高;因为你遇到过一次,解决了之后记忆能保存很长时间,除非又过了一段时间后又遇到这个问题;或者问题解决方案比较复杂,不是简单一次就能记住的。

    如果你经常出没于论坛与问答、贴吧之间的话,偶尔会看到别人遇到类似的问题,也可以用到。

    你也可以申请博客专栏,页面左侧可以看到,神马?被转载了,左侧看不到!我还是贴张图吧,顺便加个链接!

    出书!没错,经过长期的积累,CSDN博客的很多大牛都将博客的内容整理出书了,而且很多不只出了一本,一本接着一本,根本停不下来,让我这个菜鸟真是那个羡慕嫉妒恨哪!

     

    2、一点小虚荣

    当你遇到一个问题,搜索结果是自己的博客文章;
    当你的同事遇到问题,搜索结果是你的文章;

     

    当你的名称出现在全球最大中文IT社区的博客首页上;

    你是不是有那么一点小小的虚荣心和自豪感呢?

    轻拍轻拍,我就是这么没出息,原谅我吧!

    3、帮助自己

    记录分享实际是帮助别人的事情,但我定义为帮助自己;因为通过分享,帮助了别人,通过别人的留言,自己得到肯定,得到快乐;另一方面,如果有不足之处,别人会帮你指出甚至给出完善的意见,帮助完善提高。

    4、为自己的人生留下一点痕迹

    这实际是一个知识积累的过程,但更重的是为自己的人生留下一点痕迹。

    你也可以把它当成写日志,只不过是程序员体的。

    5、结交朋友

    由于之前的博客更多的记录问题,解决问题,很少有个人观点思想的表达,我还是处于一个相对比较封闭的状态,别人无法了解我,所以这方面好像没有什么效果,小伙伴们有没有什么想说的?希望不要吝啬!

    6、提高知名度?

    这个目前来说没啥感觉。

    7、提高文档能力

    这个嘛,确实有一定的帮助;但对于程序方面的文档来说,文字是一方面,画图是另一方面(思维导图,关系图,架构图之类的)。

    不过目前也就普通的文档还行,招投标文档感觉写起来还是有难度。

    8、碎片化学习

    你是否有这样的困惑?通过网络确实能够学习到很多东西,但从网上学到的东西大都是零零散散的,不系统,好像学了很多东西,又好像什么都没学到,或者学过之后又忘了,这该怎么办?

    有个人的博客就很简单了,我们可以把学习的知识按自己的结构整理记录下来,方便日后查阅巩固。

    9、提升思维能力

    这让我想到教育现象学里面的一个观点“写作即思考”,王竹立教授的《碎片与重构 互联网思维重构大教育》一书中提到此观点,如果你感兴趣可以点击链接去查看。写不出来东西的原因有两个,一是没有养成写作的习惯,二是没有养成思维的习惯,根据写作即思考的观点,归根结底一句话,就是没有学会思考。学会写作就等于学会思考,学习思考从学习写作开始。

    10、习惯

    上面提到了习惯,习惯是个很可怕的东西,一旦养成之后就很难戒掉,这个吸烟的小伙伴应该有深刻的体会;这里要说一下我对习惯的理解:就拿吸烟戒烟来说吧,我有一段时间也是吸烟的,最多的时候一天也得一盒烟(当然吸烟的小伙伴都知道,自己那盒并不是自己一个人吸了,几个人在一起的时候,有一个吸的肯定是要发圈的嘛,你发给别人,别人也会发给你),这大概有一年左右的时间;但我后来还是戒掉了,刚开始的时候确实是很不适应。那怎么才能比较容易的戒掉或改变一个习惯呢?我觉得就是要形成一个新的习惯,用新的习惯去代替旧的习惯,新 的一旦养成,旧的自然渐渐的就忘却了。

    11、专家福利

    这一块放在了最后,因为这是重头戏,压轴的!

    具体细则参考:博客专家福利

    细则中有的就不说了,说说没有的,如:C币奖励,这个很多人都知道了吧,不知道的看这个:什么是C币如何获得C币,在页面后边,没有菜单也没有锚链,看起来不爽。

    图书奖励,每月原创优质博文十篇及以上的奖励图书一本(1000C币以内商城自选),还有100C币的奖励;最初的时候是奖励当期程序员杂志一本,后来纸制的不办了推出了C币机制;现在纸制的程序员又开始办了,不过我还没见到新版的什么样子。

    另外还可以参加一些线下的活动,和大牛近距离接触!

    12、分享与学习

    通俗的讲叫分享,往高层与深远了讲,也就是讲的高大上一点,这就是佛教的讲法,基督教的布道!

    什么意思呢?也就是随着你的分享越来越多,可能会出现一批技术的追随者、崇拜者。

    当然,我的技术还远示达到那个层次,这也就是为什么是分享与学习;

    就是说,你分享的内容如果有说的不对的地方,考虑的不周的地方,角度不全面的地方,热心的小伙伴会帮你指出来!

    这样就通过你的分享,产生沟通与交流的介质,从而学习更多的知识。

     

    13、帮大家节省时间,帮助大家打胜时间这场仗。

    为什么这么说? 俗话说:师父领进门,修行在个人。

    我的博客虽然都是写的入门性的比较基本的东西,但隔行如隔山,入门实际上是最难的,一旦你入门了,再深入学习提高就简单了。

    遇到什么问题、想实现个什么功能,只需要搜索一下,百分之九十几的都能解决。

    所以我的亮点就是将入门做细做精,做到甚至是不会电脑的人也能一看就懂。

    一方面为只学了理论知识没有实战经验的;

    一方面是为已经懂一或两门语言,还想了解其它语言的;

    另一方面:O2O已经不再新鲜,万物互联的时代就将到来,但我们程序员不是业务专家,360行行行出状元,业务专家才更容易设计出他们真正想要的东西,而他们应该是更需要这样的服务。

     

    结束语

     

    看过开篇的都肯定知道了,写博客是非常花费时间的,注意是花费时间,不是浪费时间,因为我觉得这是值得的。为什么值得呢,我给出了11个让我坚持的理由。那么你呢?你现在有没有开始写博客?如果没有开始,找没找到让自己开始的理由呢?不需要太复杂太充分,正如我的简简单单就可以。如果你已经开始了,你开始的理由是什么?又是什么让你坚持呢?

    2016-04-28更新

    在另一篇博文《有学历的程序员永远不懂没学历的痛,就像白天不懂夜的黑》中黄杉问到:为啥刷blog?

    为什么放在这里来回答,因为结果都是产生博客文章,有很大的相关性。

    我首先访问了一下他的博客,看到他是一位资深的博客专家,原创文章也将近五百篇了,相信他肯定有着自己独特的答案。

    但他用的词是刷Blog,这和写博客似乎还是有着本质上的区别,虽然结果都是产生博客文章!

    刷Blog,这让我想到很多人没事刷微博,从表象上来说,给人的直接印象就是在刷存在感。

    刷,像刷屏,刷贴之类的,带有速度快,质量低,无意义,打广告的感觉。

    这让我想到了让我坚持的第12点:分享与学习。

    个人网站

    为了博客样式、功能及内容等更自由可控,我在香港主机上搭建了自己的个人网站,欢迎大家光临。

     

     

     

    展开全文
  • 年总结():系列文章目录

    万次阅读 热门讨论 2009-06-08 00:38:00
     未来我想活的更精彩,仅仅是物质的,我该怎么走,故有此反思。 年总结-开篇:歇一歇,才能走的更远 年总结(一):工作指数,寻找自信 年总结(二):压力指数,只要被压垮,压力就是动力 年总结...
  • 【从开始学习Go语言】.基础算法之冒泡排序 借用菜鸟教程中的一张图 冒泡排序,顾名思义就是像冒泡一样进行排序,那么是怎么个冒泡法呢? 举个例子说明一下,比如有一个数组:[3 2 1 0],需要将该数组进行升序...
  • 本文由@星沉阁冰语出品,转载请注明作者和出处。文章链接:http://blog.csdn.net/xingchenbingbuyu/article/details/53704085微博:http://weibo.com/xingchenbing 之前的五篇博客讲述的内容应该覆盖了如何编写...
  • 白话空间统计三:假设(补记)

    千次阅读 多人点赞 2015-08-24 14:21:45
    我的设想是对空间统计进行科普型的描述,结果到后面,特别是这几章(准确说是从填中数中心的算法坑开始),幸好有吴道长果断提醒,说我偏离方向了,我才豁然省悟。再次友情感谢吴道长(PS:吴道长是GIS圈子里面古...
  • 本篇讨论的现象可以从下面这段脚本体现出来: >>> x = 0.0 >>> for i in range(10): x += 0.1 print(x) 0.1 0.2 0.30000000000000004 0.4 0.5 0.6 0.7 0.7999999999999999 ...即:为什么有几行的输
  • 解题报告 (四) 数DP

    千次阅读 2021-04-24 05:56:56
    DP解题报告
  • 众所周知,每种基本数据类型都有一个固定的位数,比如byte占8,short占16,int占32等。正因如此,当把一个低精度的数据类型转成一个高精度的数据类型时,必然会涉及到如何扩展位数的问题。这里有两种解决方案...
  • 在前面的二五篇文章中,我介绍了从开始搭建一整套的个人网站,也有很多同学留言能能分享下源代码。 其实,如果认真看我的每篇博文的话,倒也是能自己出来的,毕竟每个人的编码习惯也是有点一样的。 其实...
  • 距离一篇博客又过去了大半年,世事难料,特别是今年半年突发新冠肺炎,打乱了很多人、公司的计划和节奏,多难兴邦,目前疫情即将完全过去,今天正好是5.20,在这个特殊的日子小有感慨,加之如今年、创业五...
  • 从事软件开发已近二十年的历史,从最初的DOS平台下的Basic、C、Foxbase,到现在的Windows平台下的VB、VC、C#,顺着微软技术主的技术路线一路走来,规模的大型程序(千行过万行的代码)不少,小程序更是多如...
  • 值比较–BOOL,int,float,指针变量与值比较的if语句这是程序员面试的一道常见题,也是个C++基础问题。若只在大学里看过几本基础的编程入门书,看见这道题可能会觉得奇怪,就是和0比较吗,直接拿出来比就是了...
  • 、从头到尾彻底理解傅里叶变换算法、

    万次阅读 多人点赞 2011-02-20 23:36:00
    经典算法研究系列:、从头到尾彻底理解傅里叶变换算法、作者:July、dznlong 二一一年二月二日推荐阅读:The Scientist and Engineer's Guide to Digital Signal Processing,By Steven W. Smith, Ph.D。...
  • 【答案】 const float EPSINON = 0.00001;  if ( (x >= -EPSINON) && (x ...可将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“ 解析: 一个区间内有无限个实数,因此计算机
  • 进制小数转化二进制小数

    千次阅读 2019-03-04 15:01:19
    由二进制数转换成进制数的基本做法是,把二进制数首先成加权系数展开式,然后按进制加法规则求和。这种做法称为"按权相加"法。例1105 把二进制数110.11转换成进制数。 二、进制数转换二...
  • ▼先说你看到的页面,最重要的几个: 【搜索商品】这个功能,如果你有几千条商品,完全可以用select * from tableXX where title like %XX%这样的操作来搞定。但是——当你有10000000000(一百亿)条商品的时候,...
  • 小甲鱼基础入门学习python笔记

    万次阅读 多人点赞 2019-08-14 11:06:30
    小甲鱼老师基础入门学习Python全套资料百度云(包括小甲鱼基础入门学习Python全套视频+全套源码+全套PPT课件+全套课后题及Python常用工具包链接、电子书籍等)请往我的资源... 000 愉快的开始 ...
  • 初学者开始代码先从main之后的花括号内编写代码 了解了函数的概念一个功能的实现 了解了printf函数的使用方法,在函数内传入值即可在程序运行时显示 知道了头文件的位置及函数存放于头文件内 了解了stystem函数...
  • 1)“基础”意味着我提供的学习路线能太难,或者说应该是循序渐进的。 2)“自学”意味着我提供的学习素材能太枯燥,或者说应该是幽默风趣的。 3)“找到工作”意味着学习周期要尽可能的短,但还要学有所成...
  • 当然如果落下什么了欢迎大家评论指出 目录 顺序存储线性表实现 单链表带头标准c语言实现 单链表带头压缩c语言实现 约瑟夫环-(数组、循环链表、数学) 线性表表示集合 线性表实现一元多项式操作 链表环问题 ...
  • 程序员的年之痒

    万次阅读 多人点赞 2020-11-09 08:27:52
    一路回忆过来,发现我应该能算是个正经程序员,这后来的一切都源于年前的一个传单。 那时候在读高中二年级,那个中午刚走到学校大门外,阳光正好通过那个传单刺到了我的眼睛,传单的内容很长,是一个技术学院...
  • 程序员的年工作总结,值得每互联网人看

    万次阅读 多人点赞 2017-12-13 22:45:59
    但是时间长了,你一回过头来想想,你为什么赚得也不少,但是为什么还是那么穷,是因为你购买这些电子产品花费了过多的金钱了,平时笔记本啥的贵重物品要保护好,我一个同事小心丢了2台笔记本电脑,接近2万的损失啊...
  • 夜深人静算法(九)- Dancing Links X(跳舞链)

    万次阅读 多人点赞 2018-01-31 19:56:44
     第1)条很容易理解,倘若存在某两个数的“与”等于0,那么在这两个数的二进制表示中势必存在某一1,即一列至少有两个“1”,满足题目要求;第2)条可以这么理解,所有数的“或”等于2^C - 1,代表...
  • 三章、其他功能完善 1、清除发送数据内容 ⑴添加清楚发送槽函数,选择clicked()。   ⑵添加清除发送数据处理程序。   2、串口断开功能实现 ⑴创建串口断开函数   ⑵串口断开函数实现   ⑶添加...
  • IT博士的大学

    千次阅读 2009-09-01 21:56:00
    大学年文/林聪 此文使我很为难,一是担心读者误以为我轻浮得现在就开始自传,二是担心朋友们误以为我得了绝症而早早留下遗作。不论是落俗套还是不落俗套地评价,我在大学年里都是出类拔萃的好学生。并且...
  • 开始学习Linux笔记

    万次阅读 多人点赞 2020-05-15 19:12:14
    开始学习Linux,记录笔记,担心自己以后会忘,也供大家茶余饭后,闲来无事看看,自己的理解只能到这,也希望大家可以指出我的错误 让我可以有一点点进步,以后会一直更新
  • 忙忙碌碌将近一年,我的新书《Android Studio开发实战 从基础到App上线》终于正式出版上市了,拿到样书的那一刻,看着制作精美的书页,掂着沉重厚实的感觉,真是喜不自胜。本书能够顺利出版面世,要感谢我的家人在...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 88,194
精华内容 35,277
关键字:

十位上的零为什么不写