Linux系统编程4(进程信号详解)

你知道为什么当程序中出现除0就会引发程序崩溃退出吗?你知道为何在Linux中输入kill -9 pid 就能杀死进程id为pid的进程吗?这篇文章将详细探讨解答这些问题,文章内容比较长,大家可以收藏慢慢看

什么是信号 

在进程间通信这篇文章中,我们学习过信号量这个概念,这里跟大家说一下,信号量和信号完全是两个概念,两者之间没有什么关系。那信号是什么呢?生活中我们常见的信号有信号弹,有红绿灯,看到信号弹,我们就知道了接下来要怎么行动了,看到红绿灯,我们就知道接下来是该走还是该停了,包括各位同学女朋友的脸色,脸色一变就能明白接下来该是讲道理的时间了

总结一下信号,信号不仅仅是一种现象,还包括出现这种现象接下来该如何操作的方法,是对即将或者可能出现的某种现象的应对,这样说略显抽象,其实就是当操作系统给进程发送某种信号时,进程收到这种信号就要做出相应的处理,处理方法是程序员预先编写好的,当出现这种情况,直接调用就好了。就像等红绿灯一样,当大脑收到红灯这个信号,就知道该停下来,因为我们提前接受过红灯停绿灯行这种教育

怎么判断进程是否收到了操作系统发给它的信号,以及对这种信号做出相应的处理了呢?

我们给正在运行的一个进程发送信号,看看进程收到信号有什么变化就可以验证了?

接下来写一个测试demo,代码如下

#include
#include
#include

int main(){

    while(true){
        sleep(2);
        printf("pid: %d is waiting signal...\n", getpid());
    }

    return 0;
}

 测试结果如下,test程序正在运行的时候,我们通过root用户给该进程发送9号命令,从结果上看该进程收到了信号,并且执行了进程结束的方法

Linux系统编程4(进程信号详解)_第1张图片

这里给大家介绍一下,我们可以通过命令 kill - l 查看可以给进程发送哪些信号,总共有64个信号,前32个属于普通信号,是需要我们花时间了解的,34-64就是属于实时信号,不是我们目前学习的重点,因此本篇文章只涉及1-32个信号,这并不意味着我们要全部了解这32个信号,这样篇幅臃肿没有必要,对几个信号学习后,就具备查阅使用其他信号的能力

Linux系统编程4(进程信号详解)_第2张图片

如何给进程发送信号

1.命令行与组合键形式

前面我们提到过,也是大家经常用到的一个方法就是通过shell命令行给进程发送信号,这就会用到 kill 命令,还有通过快捷组合键的形式,例如CTRL+C,CTRL+\,CTRL+D等等

通过kill命令给进程发送信号的格式为 kill -num pid

其中num表示信号的序号,可以通过kill -l查看,pid是指要被发信号的进程的id(未来参数中出现pid,笔者不再重复说明,默认指进程的id)

ctrl+c:热键 --- 本质是一个组合键 -> os -> os将ctrl+c解释成2号信号 2号信号就是终止程序

ctrl+\:热键 --- 本质是一个组合键 -> os -> os将ctrl+\解释成3号信号 3号信号也是终止程

 2.程序内部的相关系统调用

首先就是比较重要的kill()函数,kill()可以给任意进程发送任意信号

这个函数就两个参数,一个是进程的id,另一个是要发送什么信号,这个可以用信号的序号,也可以用信号名来表示

Linux系统编程4(进程信号详解)_第3张图片

接下来我们写2个简单的demo来演示kill函数的用法

第一个测试内容是让进程给自己发送发送一个9号命令 

第二个测试内容是让进程A给进程B发送一个9号命令(因为进程A用kill给进程B发送信号,就要知道进程B的id,这里笔者偷个懒,不在A与B之间建立通信,而是让A与B为父子进程,目的是一样的,都能演示出kill函数的效果)

                           //demo 1
#include
#include
#include


int main(){

    int count = 5;
    while(count--){
        sleep(2);
        printf("pid: %d is waiting signal...\n", getpid());
        if (count == 1) kill(getpid(), 9);
    }

    return 0;
}

Linux系统编程4(进程信号详解)_第4张图片

                                 //demo  2
#include 
#include 
#include 


int main()
{

    pid_t id = fork();
    if (id > 0)
    {
        // 休眠十秒后,父进程将给子进程发送9号信号
        sleep(10);
        kill(id, 9);
    }

    if (id == 0)
    {
        while (true)
        {
            sleep(2);
            printf("childpid: %d is waiting signal...\n", getpid());
        }
    }

    return 0;
}

Linux系统编程4(进程信号详解)_第5张图片

通过两个实例,相信大家可以很轻松的掌握kill函数的用法,接下来我们继续学习两个常用的的发送信号的系统调用

raise()给自己发送任意信号 

abort()给自己发送指定信号 SIGABRT 

这两个函数其实都可以用kill函数来实现,例如raise函数,就等于 kill(getpid(), signo)

abort函数,就等于 kill(getpid(), SIGABRT)

会了kill函数的用法,raise和abort的用法自然不在话下

如果你尝试过给进程发送不同的信号,会发现进程接收到信号大部分的处理动作都是终止进程,不同的信号代表着因不同的原因导致进程终止,一般来说,进程受到信号就意味着进程运行时出现了问题或者进程即将结束,再运行下去没有必要,因此进程收到信号的默认动作就是终止进程,接下来我们逐步了解什么情况下,进程会收到系统发送的信号

不过在此之前,咱们来看看进程到底是怎么收到信号的,OS又是如何把信号传递给进程的

Linux系统编程4(进程信号详解)_第6张图片

 Linux系统编程4(进程信号详解)_第7张图片

信号位图在OS中的名称为pending位图,关于进程接收信号及处理过程笔者后续会详细介绍,这里简单理解即可

 进程信号捕捉

前面说了那么多,到底是理论而已,我们要通过实践去检验理论,OS到底有没有给进程发送信号,我们站在进程的角度,到信号就能证实前面的理论

可以通过signal()来接收OS发来的信号

Linux系统编程4(进程信号详解)_第8张图片

signum表示接收哪个信号, sighandler_t是一个函数指针类型,handler即是函数指针,这个函数表示收到signum信号后的处理方法,系统默认的处理方法是终止进程,在shell界面按下快捷键CTRL+c可以终止进程,即给进程发送SIGINT信号

接下来通过一个demo来演示该函数捕捉SIGINT信号,然后按我们的操作来执行

#include 
#include 
#include 
#include 

using namespace std;

void handler(int signo)
{
    printf("SIG: %d was captured by process: %d\n",signo, getpid());
    sleep(3);
    return;
}

int main()
{
    signal(SIGINT, handler);

    while(true){
        sleep(1);
        printf("waiting signal...\n");
    }
   
    return 0;
}

Linux系统编程4(进程信号详解)_第9张图片 

由运行结果可得知,进程确实捕捉到了2号信号,并执行了我们的handler函数

除了signal(),还有sigaction() ,这个函数用法更复杂一点,但是可操作性更高

#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

这个函数的介绍笔者放到后面,里面一些参数目前没法使用

产生进程信号的情况

硬件原因

1.最典型的就是/0问题,看下面一段代码,当运行该demo时系统就会报错

#include

using namespace std;


int main() {

    int test = 1, tmp = 0;
    //test /= 0;  这种写法无法通过编译
    test /= tmp;
    printf("%d", test);
    return 0;
}

Linux系统编程4(进程信号详解)_第10张图片

运行后直接报错,程序终止,可以猜测出这是进程收到了系统发给它的某个信号,从而造成它停止运行,一起来分析这个过程,如下图

 Linux系统编程4(进程信号详解)_第11张图片

2.解引用空指针导致硬件工作异常从而产生信号

解引用野指针是编程学习过程中进程会遇到的问题,等到编译运行报错了,我们才反应过来,那个时候我们更改错误然后就不管了,今天我们从底层深刻来理解为什么解引用空指针程序就会终止报错

Linux系统编程4(进程信号详解)_第12张图片

上图是我们的老朋友了,虚拟地址空间通过页表的映射转换成物理地址空间,不过这个转换的过程是由谁来完成的,之前并没有说,这个将虚拟地址空间转换成物理地址空间是由MMU负责的,不过MMU并不是和页表在一起,而是集成到了CPU中,当CPU读取到虚拟地址空间时,就会自动在内部通过MMU转换成了物理地址,如下图

Linux系统编程4(进程信号详解)_第13张图片

程序因为访问的是一个空地址,那么负责给CPU解析页表映射地址的硬件MMU能检测到这个空地址错误并报给OS,OS便给进行这个解引用的程序发送错误信号SIGSEGV,从而导致进程报错退出,过程如下图

Linux系统编程4(进程信号详解)_第14张图片

软件原因 

1.还记得前面谈论过的进程间的通信吗?那个时候笔者提到过,管道通信时,读端如果关闭了,那么操作系统为了节省系统资源,会自动关闭写端,这个过程就是OS给写端发送SIGPIPE信号,让其停止写入

2. alarm形式的信号,alarm字面意思就是闹钟,人可以被闹钟叫醒,进程也有软性形式的闹钟,当闹钟响了,OS就会给指定闹钟的进程发送SIGALRM信号

unsigned int alarm(unsigned int seconds);  

利用这个函数,我们写出检测出电脑一秒钟大概可以计算多少次的demo,代码如下

#include 
#include 
#include 
#include 

using namespace std;

long long count = 0;

void handler(int signo)
{
    printf("计算次数为:%lld\n", count);
    exit(0);
}

int main()
{
    alarm(1); //设定一秒后的闹钟
    signal(SIGALRM, handler);  //等待接收alarm信号

    while(true){
       count++;
    }     //检测闹钟响前,count被运算的次数
   
    return 0;
}

Linux系统编程4(进程信号详解)_第15张图片

根据运行结果可知,我的服务器一秒大概可以运行4亿多次,但这可不是CPU的运行速度,因为CPU会有时间片轮转,这个只能大概测出一个进程一秒内能被CPU执行多少次

由这个demo可见,alarm还是比较好用的,任意一个进程都可设置alarm,那么可想而知OS中会存在大量的进程设置了alarm,OS就要有对应的结构来管理好这些alarm,等到时间到了,再唤醒对应的进程,给其发送alarm信号,结构大致如下图

Linux系统编程4(进程信号详解)_第16张图片

核心转储 

不捕捉信号的时候,系统给进程发送信号会执行默认的行为,这个行为一般就是终止进程,也不全是直接终止,这个我们可以通过命令man 7 signal来查看

Linux系统编程4(进程信号详解)_第17张图片 

可以发现,标Term的信号默认行为都是终止进程,还有Core, stop(暂停进程)等等,标Core的信号表示支持核心转储,可以发现SIGSEGV信号默认处理是核心转储,数组越界写入就会产生SIGSEGV信号,我们写一个越界demo看看会发生什么

#include 
#include 
#include 

using namespace std;

int main()
{
    int a[20] = {0};
    a[10000] = 300;
    return 0;
}

 Linux系统编程4(进程信号详解)_第18张图片

 除了报一个段错误,好像并没有什么特别的现象,核心转储体现在哪呢?这是因为核心转储在云服务器上默认是关闭状态的,我们需要手动打开

使用命令ulimit -a 可以查看核心转储文件的大小,如图默认为0

使用命令ulimit -c 2048 将核心转储文件的大小设置为2048

修改完成后,再次运行前面越界的demo

Linux系统编程4(进程信号详解)_第19张图片

神奇的现象出现了, 打开核心转储文件后,再次执行越界程序,除了报了段错误,还多个一个文件,这个文件就是核心转储文件,所以核心转储就是在进程出现异常的时候,进程在对应的时刻将进程的有效数据转储到磁盘中,形成的文件就是核心转储

通过gdb,可以使用核心转储文件,操作如下

Linux系统编程4(进程信号详解)_第20张图片

通过核心转储文件,可以直接定位到错误地点,不用一步一步调试了,记得在编译代码文件时加上-g即支持调试

信号阻塞 

pending和block位图

OS发来的信号,我进程就一定,必须,无条件的执行吗?就不能在接收信号后将其屏蔽,不执行其对应的方法吗?

当然可以,接下来就学习信号阻塞,看到这里不知道大家伙还记得前面提到过的pending位图吗?那时说的比较简单,就认为进程中的信号都保存在pending位图中,pending位图中的比特位被置为1,就表示收到该信号,然后就立即执行对应的处理方法。事实上,pending位图是一种未决状态,意思是pending位图某个信号被置为了1,表示进程确实收到了这个信号,但是并没有执行,而是等待OS的内核来执行,阻塞信号的原理就是在OS内核准备查看pending位图中哪些信号需要被执行时,在中间加了一道锁,即使进程收到了某个信号,其对应的pending位图也被置为了1,只要阻塞该信号,就相当于给该信号加了一道锁,OS内核一看有把锁就直接走了,然后该这个信号就一直处于未决状态,直到取消该对该信号的阻塞

Linux系统编程4(进程信号详解)_第21张图片

阻塞信号同样是用位图来表示的,名称为block位图,下图把二者对应起来分析 

Linux系统编程4(进程信号详解)_第22张图片

 sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志(block)也是这样表示的
因此,未决(pending)和阻塞标志(block)可以用相同的数据类型 sigset_t 来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略 

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的,sigset_t要配合下列函数调用来使用

信号位图处理函数 

#include 

int sigemptyset(sigset_t *set);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,
表示该信号集不包含 任何有效信号


int sigfillset(sigset_t *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,
表示该信号集的有效信号包括系统支持的所有信号


int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,
使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用
sigaddset和sigdelset在该信号集中添加或删除某种有效信号


int sigismember(const sigset_t *set, int signo);
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含
某种 信号,若包含则返回1,不包含则返回0,出错返回-1


int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集),oset是一个输出型参数,
如果oset是非空指针,则读取进程的当前的信号屏蔽字(block位图)保存到oset中。
如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,
然后根据set和how参数更改信号屏蔽字


int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
set为输出型参数


int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
//这是前面提到过的信号捕捉的另一个函数

下图说明sigprocmask函数中how参数如何填写,假设当前的block位图为mask

Linux系统编程4(进程信号详解)_第23张图片

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达

接下来详细介绍一下sigaction()的用法

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号,若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

说白了,oact就是当前设置的一个备份,是一个输出型参数

可见想用好这个函数,还得知道struct sigaction里包含了什么字段 

Linux系统编程4(进程信号详解)_第24张图片

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用 

这么多函数想一次消化掉可不容易,也不要想着去清晰的记住他们,如果它们的使用够高频你绝对忘不了,如果使用不够高频,用到时再查,但是要有印象,知道怎么用

接下来通过demo来体验这几个函数的用法  

#include 
#include 
#include 
#include 
#include


void handler(int signal){

    printf("已经捕捉到2号信号了\n");
}

int main()
{
    //创建并初始化3个位图,block_backup用来备份block位图
    sigset_t test_block, test_pending, block_backup;
    sigemptyset(&test_block);
    sigemptyset(&test_pending);
    sigemptyset(&block_backup);


    //在test_block位图中添加2号信号,并通过sigprocmask()
    //将test_block置为当前进程的block位图
    sigaddset(&test_block, 2);
    sigprocmask(SIG_SETMASK, &test_block, &block_backup);

    signal(2,handler);

    while(true){
        printf("进程:%d 正在等待2号信号,并执行handler函数\n", getpid());
        sleep(2);
    }

    return 0;
}

Linux系统编程4(进程信号详解)_第25张图片

通过运行结果可知,2号信号还真被阻塞了,通过kill给进程发送多次2号信号,handler函数并没有被执行,为了证明2号信号真被收到了,但是被阻塞一直处于未决状态,咱们就要把pending位图给打印出来看看,为了方便,咱们就不用kill发送2号信号了,直接用CTRL+c

#include 
#include 
#include 
#include 
#include


void handler(int signal){

    printf("已经捕捉到2号信号了\n");
}

void show_pending(sigset_t *pending){
    
    for (int i = 1; i<= 32; i++){
        
        if (sigismember(pending, i) == 1) printf("1");
        else printf("0");       
    }

    printf("\n");
}

int main()
{
    //创建并初始化两个位图
    sigset_t test_block, test_pending, block_backup;
    sigemptyset(&test_block);
    sigemptyset(&test_pending);


    //在test_block位图中添加2号信号,并通过sigprocmask()
    //将test_block置为当前进程的block位图
    sigaddset(&test_block, 2);
    sigprocmask(SIG_SETMASK, &test_block, &block_backup);

    signal(2,handler);

    while(true){
        
        //打印当前进程pending位图
        sigpending(&test_pending);
        show_pending(&test_pending);

        printf("进程:%d 正在等待2号信号,并执行handler函数\n", getpid());
        sleep(2);
    }

    return 0;
}

 Linux系统编程4(进程信号详解)_第26张图片

可以发现,当我们发送2号信号时,pending位图的2号位置由0变为1,说明2号信号确实被阻塞一直处于未决状态,通过这么一个示例,就可以把这几个信号阻塞函数应用,希望大家可以自行编写该测试代码 

接下来看看sigaction函数的用法

 

#include 
#include 
#include 
#include 
#include


void handler(int signal){

    printf("已经捕捉到2号信号了\n");
    exit(0);
}


int main()
{
    sigset_t test_pending;
    sigemptyset(&test_pending);

    //设置sigaction结构的相关参数
    struct sigaction test_1, test_2;
    test_1.sa_handler = handler;
    test_1.sa_mask = test_pending;
    test_1.sa_flags = 0;
    //其余参数不需要操心

    //捕捉2号信号
    sigaction(2, &test_1, &test_2);

    while(true){

        printf("正在等待信号传递\n");
        sleep(1);
    }


    return 0;
}

 Linux系统编程4(进程信号详解)_第27张图片

 

信号捕捉及处理的详细流程 

尽管上文对信号的讲解足够大家日常使用了,但是大家心中可能仍然对信号捕捉处理过程中OS的具体做法心存疑惑,接下来我们就从头开始理解整个信号的捕捉和处理流程

我们前面对OS的理解都把它作为一个整体,现在要具体划分一下OS,OS其实分为内核态和用户态,什么是内核态?什么又是用户态?

用户态:像我们用户平时写的一些测试代码,一些算法代码等等,虽然被加载到内存运行了,但一直是运行在OS的用户态,在用户态下,用户程序只能访问受操作系统授权的资源和执行受限的操作,不能直接访问底层的硬件资源。用户态提供了一种安全的环境,使得用户程序无法对系统造成损害,同时也限制了用户程序的权限

内核态:是指操作系统内核运行的部分,内核拥有最高的权限,可以直接访问和操作系统底层的硬件资源。在内核态下,操作系统可以执行特权指令和访问敏感资源,可以对硬件和系统资源进行管理和控制

当用户程序需要访问需要特权的操作或资源时,例如进行系统调用、访问硬件设备或进行特定的内核操作时,会触发一个从用户态到内核态的切换。在内核态中执行完成所需的操作后,又会切换回用户态,将结果返回给用户程序

Linux系统编程4(进程信号详解)_第28张图片

 

如何以全局的视角看待程序执行从用户态进入到内核态呢?前面我们一直说用户的代码保存在进程地址空间的代码区,而进入内核态则需要执行内核的代码,这个过程是如何跳转的

 Linux系统编程4(进程信号详解)_第29张图片

 

关于页表,我们并没有完全探明其结构,平时我们都是以一张页表来映射所有的虚拟地址,事实并非如此,页表也是分为用户级页表和内核级页表的 

Linux系统编程4(进程信号详解)_第30张图片 

有了上述知识的铺垫,就能把整个信号捕捉的过程走一遍了 

Linux系统编程4(进程信号详解)_第31张图片 

整个过程和下面这张流程图十分相像

 Linux系统编程4(进程信号详解)_第32张图片

volatile

volatile是C语言中的一个关键字,在平时的代码练习中,它的出场率并不高,很多同学包括笔者几乎忘记C语言还有这么个关键字。不过既然能作为关键字出现在一个语言中,可见其作用还是不凡的,只是目前我们接触不到其使用场景

在进程信号中,我们可以感受到volatile其中的一个应用场景

看下面一段代码

 

​
#include 
#include 
#include 
#include 
#include

using namespace std;

int flag = 0;

void handler(int signal){
    flag = 1;
    printf("已经捕捉到2号信号了\n");
}

int main()
{
    signal(2, handler);
    
    while(!flag);

    printf("程序开始退出\n");
    return 0;
}

​

Linux系统编程4(进程信号详解)_第33张图片

结果符合预期,接下来提高编译器的优化级别,不动代码,修改makefile即可 

Linux系统编程4(进程信号详解)_第34张图片

Linux系统编程4(进程信号详解)_第35张图片

神奇的事情发生了,提高了代码的优化级别,给进程发送2号信号,进程却不退出了 ,flag不应该被置为1了吗?那么!flag就是0啊,为什么循环不退出了呢?

Linux系统编程4(进程信号详解)_第36张图片

Linux系统编程4(进程信号详解)_第37张图片 

Linux系统编程4(进程信号详解)_第38张图片 

SIGCHLD信号

在进程部分提到过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了。采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂
 

事实上子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可


这个方法对单个子进程比较好用,当遇到多个子进程时,如果多个子进程同时退出,那么就要循环阻塞wait,如果多个子进程部分退出,部分不退出则循环不阻塞wait

想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的

至此,本篇文章就结束了,信号这篇内容比较繁多,事实上,系统编程就不是省油的灯,其不仅要掌握编程知识,对计算机体系结构的要求也很高,想要熟练掌握实属不易。虽然知识比较繁杂,好在写一些底层的代码不断打消曾经学习编程时的疑惑,逐渐有种茅舍顿开感。希望各位能在学习的过程中找到属于自己的那份喜悦,不为前途,不为功名,为的是纯粹求知的满足

你可能感兴趣的:(Linux(基础使用,系统编程,网络编程),linux,运维,服务器)