扩展阅读

224 阅读12分钟

Core Dump

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁 盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许 产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit - c 1024

然后写一个死循环程序:

  • 前台运行这个程序,然后在终端键入Ctrl-C( 貌似不行)或Ctrl-\(介个可以):
  • ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相 同的Resource Limit值,这样就可以产生Core Dump了。 使用core文件

信号操作

sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

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

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); 

  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有 效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系 统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信 号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

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

sigpending

#include <signal.h>
sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。
程序如下: 

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void printsigset(sigset *set){
    int i=0;
    for (;i<32;i++)
    {
        //判断信号是否在目标集合中
        if(sigismember(set,i)){
            putchar('i');
        }else{
            putchar('0');
        }
    }
    puts("");
}
int main()
{
    sigset_t s,p;
    sigemptyset(&s);//定义信号集集合,并清空初始化
    sigaddset(&s,SIGINT);
    sigprocmask(SIG_BLOCK,&s,NULL);
    while(1){
        sigpending(&p);
        printsigset(&p);
        sleep(1);
    }
    return 0;
}

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会 使SIGINT信号处于未决 状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 

  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传 出该信号原来的处理动作。act和oact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动 作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都 把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。

volatile[C讲过,选学]

  • 该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
   printf("chage flag 0 to 1\n");
   flag = 1;
}
int main()
{
   signal(2, handler);
   while(!flag);
   printf("process quit normal\n");
   return 0;
}
[hb@localhost code_test]$ cat Makefile 
sig:sig.c
 gcc -o sig sig.c #-O2
.PHONY:clean
clean:
 rm -f sig
[hb@localhost code_test]$ ./sig 
^Cchage flag 0 to 1
process quit normal
  • 标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循 环,进程退出
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
   printf("chage flag 0 to 1\n");
   flag = 1;
}
int main()
{
   signal(2, handler);
   while(!flag);
   printf("process quit normal\n");
   return 0;
}
[hb@localhost code_test]$ cat Makefile 
sig:sig.c
 gcc -o sig sig.c -O2
.PHONY:clean
clean:
 rm -f sig
[hb@localhost code_test]$ ./sig 
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1

优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足, 进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的 flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放 在了CPU寄存器当中。如何解决呢?很明显需要 volatile

[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
}
int main()
{
    signal(2, handler);
    while(!flag);
    printf("process quit normal\n");
    return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.c
 gcc -o sig sig.c -O2
.PHONY:clean
clean:
 rm -f sig
 
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal
  • volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作

SIGCHLD信号 [选学]

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

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

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数, 在其中调用wait获得子进程的退出状态并打印。

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽 略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。

测试代码

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
 pid_t id;
 while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
 printf("wait child success: %d\n", id);
 }
 printf("child is quit! %d\n", getpid());
}
int main()
{
 signal(SIGCHLD, handler);
 pid_t cid;
 if((cid = fork()) == 0){//child
 printf("child : %d\n", getpid());
 sleep(3);
 exit(1);
 }
 while(1){
 printf("father proc is doing some thing!\n");
 sleep(1);
 }
 return 0;
}

进程ID和线程ID

  • 在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称 为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有 自己的进程描述符(task_struct结构体)。
  • 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情 况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有 自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线 程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
struct task_struct {
 ...
 pid_t pid;
 pid_t tgid;
 ...
 struct task_struct *group_leader;
 ...
 struct list_head thread_group;
 ...
};

  • 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符 (task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应 的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID
用户态系统调用内核进程描述符中对应的结构
线程IDpid_t gettid(void)pid_t pid
进程IDpid_t getpid(void)pid_t tgid

现在介绍的线程ID,不同于 pthread_t 类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯 一标识线程的一个整型变量。如何查看一个线程的ID呢?

[root@localhost linux]# ps -eLf |head -1 && ps -eLf |grep a.out |grep -v grep
UID       PID PPID   LWP C NLWP STIME TTY         TIME CMD
root     28543 22937 28543 0   2 15:32 pts/0   00:00:00 ./a.out
root     28543 22937 28544 0   2 15:32 pts/0   00:00:00 ./a.out

ps命令中的-L选项,会显示如下信息:

  • LWP:线程ID,既gettid()系统调用的返回值。
  • NLWP:线程组内线程的个数

可以看出上面a.out进程是多线程的,进程ID为28543,进程内有2个线程,线程ID分别为28543,28544。

Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来共程序员使 用。如果确实需要获得线程ID,可以采用如下方法:

#include <sys/syscall.h> 
pid_t tid;
tid = syscall(SYS_gettid);
  • 从上面可以看出,a.out进程的ID为28543,下面有一个线程的ID也是28543,这不是巧合。线程组内的 第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个 线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的 进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。
 /* 线程组ID等于线程ID,group_leader指向自身 */
 p->tgid = p->pid;
 p->group_leader = p;
 INIT_LIST_HEAD(&p->thread_group);
  • 至于线程组其他线程的ID则有内核负责分配,其线程组ID总是和主线程的线程组ID一致,无论是主线程 直接创建线程,还是创建出来的线程再次创建线程,都是这样。
 if ( clone_flags & CLONE_THREAD )
 p->tgid = current->tgid;
 if ( clone_flags & CLONE_THREAD ) {
 P->group_lead = current->group_leader;
 list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
 }

  • 强调一点,线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。

DNS

最初, 通过互连网信息中心(SRI-NIC)来管理这个hosts文件的.

  • 如果一个新计算机要接入网络, 或者某个计算机IP变更, 都需要到信息中心申请变更hosts文件.
  • 其他计算机也需要定期下载更新新版本的hosts文件才能正确上网.

这样就太麻烦了, 于是产生了DNS系统.

  • 一个组织的系统管理机构, 维护系统内的每个主机的IP和主机名的对应关系.
  • 如果新计算机接入网络, 将这个信息注册到数据库中;
  • 用户输入域名的时候, 会自动查询DNS服务器, 由DNS服务器检索数据库, 得到对应的IP地址.

至今, 我们的计算机上仍然保留了hosts文件. 在域名解析的过程中仍然会优先查找hosts文件的内容.

cat /etc/hosts 

域名简介

主域名是用来识别主机名称和主机所属的组织机构的一种分层结构的名称.

www.baidu.com

域名使用 . 连接

  • com: 一级域名. 表示这是一个企业域名. 同级的还有 "net"(网络提供商), "org"(非盈利组织) 等.
  • baidu: 二级域名, 公司名.
  • www: 只是一种习惯用法. 之前人们在使用域名时, 往往命名成类似于ftp.xxx.xxx/www.xxx.xxx这样的格 式, 来表示主机支持的协议.

域名解析过程(选学)

大家自行搜索资料. 可以参考 <<图解TCP/IP>> 相关章节

使用 dig 工具分析 DNS 过程

安装 dig 工具

yum install bind-utils

之后就可以使用 dig 指令查看域名解析过程了.

dig www.baidu.com

结果形如

; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7_5.1 <<>> www.baidu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41628
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.baidu.com. IN A
;; ANSWER SECTION:
www.baidu.com. 1057 IN CNAME www.a.shifen.com.
www.a.shifen.com. 40 IN A 115.239.210.27
www.a.shifen.com. 40 IN A 115.239.211.112
;; Query time: 0 msec
;; SERVER: 100.100.2.136#53(100.100.2.136)
;; WHEN: Wed Sep 26 00:05:25 CST 2018
;; MSG SIZE rcvd: 90

结果解释

  1. 开头位置是 dig 指令的版本号
  2. 第二部分是服务器返回的详情, 重要的是 status 参数, NOERROR 表示查询成功
  3. QUESTION SECTION 表示要查询的域名是什么
  4. ANSWER SECTION 表示查询结果是什么. 这个结果先将 www.baidu.com 查询成了 www.a.shifen.com, 再将 www.a.shifen.com 查询成了两个 ip 地址.
  5. 最下面是一些结果统计, 包含查询时间和 DNS 服务器的地址等.
  6. 更多 dig 的使用方法, 参见 www.imooc.com/article/269…

ICMP协议

ICMP协议是一个 网络层协议

一个新搭建好的网络, 往往需要先进行一个简单的测试, 来验证网络是否畅通; 但是IP协议并不提供可靠传输. 如果丢 包了, IP协议并不能通知传输层是否丢包以及丢包的原因.

ICMP功能

ICMP正是提供这种功能的协议; ICMP主要功能包括:

  • 确认IP包是否成功到达目标地址.
  • 通知在发送过程中IP包被丢弃的原因.
  • ICMP也是基于IP协议工作的. 但是它并不是传输层的功能, 因此人们仍然把它归结为网络层协议;
  • ICMP只能搭配IPv4使用. 如果是IPv6的情况下, 需要是用ICMPv6;

ICMP的报文格式 (选学)

关于报文格式, 我们并不打算重点关注, 大家稍微有个了解即可

ICMP大概分为两类报文:

  • 一类是通知出错原因
  • 一类是用于诊断查询

ping命令

  • 注意, 此处 ping 的是域名, 而不是url! 一个域名可以通过DNS解析成IP地址
  • ping命令不光能验证网络的连通性, 同时也会统计响应时间和TTL(IP包中的Time To Live, 生存周期).
  • ping命令会先发送一个 ICMP Echo Request给对端;
  • 对端接收到之后, 会返回一个ICMP Echo Reply;

一个值得注意的坑

有些面试官可能会问: telnet是23端口, ssh是22端口, 那么ping是什么端口?

千万注意!!! 这是面试官的圈套

ping命令基于ICMP, 是在网络层. 而端口号, 是传输层的内容. 在ICMP中根本就不关注端口号这样的信息.

traceroute命令

也是基于ICMP协议实现, 能够打印出可执行程序主机, 一直到目标主机之前经历多少路由器

Linux2.6内核进程调度队列 [选学]

一个CPU拥有一个runqueue

  • 如果有多个CPU就要考虑进程个数的负载均衡问题

优先级

  • 普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
  • 实时优先级:0~99(不关心)

活动队列

  • 时间片还没有结束的所有进程都按照优先级放在该队列

  • nr_active: 总共有多少个运行状态的进程

  • queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!

  • 从该结构中,选择一个最合适的进程,过程是怎么的呢?

  • bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个 比特位表示队列是否为空,这样,便可以大大提高查找效率!

    1. 从0下表开始遍历queue[140]

    2. 找到第一个非空队列,该队列必定为优先级最高的队列

    3. 拿到选中队列的第一个进程,开始运行,调度完成!

    4. 遍历queue[140]时间复杂度是常数!但还是太低效

      了!

过期队列

  • 过期队列和活动队列结构一模一样
  • 过期队列上放置的进程,都是时间片耗尽的进程
  • 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

active指针和expired指针

  • active指针永远指向活动队列
  • expired指针永远指向过期队列
  • 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在 的。
  • 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活 动进程!

总结

在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增 加,我们称之为进程调度O(1)算法!