只是简单的描述了一下 Linux 基本概念,通过几个例子来说明 Linux 基本应用程序,然后以 Linux 基本内核构造来结尾。那么本篇文章我们就深入理解一下 Linux 内核来理解 Linux 的基本概念之进程和线程。系统调用是操作系统本身的接口,它对于创建进程和线程,内存分配,共享文件和 I/O 来说都很重要。
我们将从各个版本的共性出发来进行探讨。
基本概念
Linux 一个非常重要的概念就是进程,Linux 进程和我们在
中探讨的进程模型非常相似。每个进程都会运行一段独立的程序,并且在初始化的时候拥有一个独立的控制线程。换句话说,每个进程都会有一个自己的程序计数器,这个程序计数器用来记录下一个需要被执行的指令。Linux 允许进程在运行时创建额外的线程。
Linux 是一个多道程序设计系统,因此系统中存在彼此相互独立的进程同时运行。此外,每个用户都会同时有几个活动的进程。因为如果是一个大型系统,可能有数百上千的进程在同时运行。
在某些用户空间中,即使用户退出登录,仍然会有一些后台进程在运行,这些进程被称为 守护进程(daemon)
。
Linux 中有一种特殊的守护进程被称为 计划守护进程(Cron daemon)
,计划守护进程可以每分钟醒来一次检查是否有工作要做,做完会继续回到睡眠状态等待下一次唤醒。
“Cron 是一个守护程序,可以做任何你想做的事情,比如说你可以定期进行系统维护、定期进行系统备份等。在其他操作系统上也有类似的程序,比如 Mac OS X 上 Cron 守护程序被称为
launchd
的守护进程。在 Windows 上可以被称为计划任务(Task Scheduler)
。
在 Linux 系统中,进程通过非常简单的方式来创建,fork
系统调用会创建一个源进程的拷贝(副本)
。调用 fork 函数的进程被称为 父进程(parent process)
,使用 fork 函数创建出来的进程被称为 子进程(child process)
。父进程和子进程都有自己的内存映像。如果在子进程创建出来后,父进程修改了一些变量等,那么子进程是看不到这些变化的,也就是 fork 后,父进程和子进程相互独立。
虽然父进程和子进程保持相互独立,但是它们却能够共享相同的文件,如果在 fork 之前,父进程已经打开了某个文件,那么 fork 后,父进程和子进程仍然共享这个打开的文件。对共享文件的修改会对父进程和子进程同时可见。
那么该如何区分父进程和子进程呢?子进程只是父进程的拷贝,所以它们几乎所有的情况都一样,包括内存映像、变量、寄存器等。区分的关键在于 fork
函数调用后的返回值,如果 fork 后返回一个非零值,这个非零值即是子进程的 进程标识符(Process Identiier, PID)
,而会给子进程返回一个零值,可以用下面代码来进行表示
pid = fork(); // 调用 fork 函数创建进程 if(pid < 0){ error() // pid < 0,创建失败 } else if(pid > 0){ parent_handle() // 父进程代码 } else { child_handle() // 子进程代码 }
父进程在 fork 后会得到子进程的 PID,这个 PID 即能代表这个子进程的唯一标识符也就是 PID。如果子进程想要知道自己的 PID,可以调用 getpid
方法。当子进程结束运行时,父进程会得到子进程的 PID,因为一个进程会 fork 很多子进程,子进程也会 fork 子进程,所以 PID 是非常重要的。我们把第一次调用 fork 后的进程称为 原始进程
,一个原始进程可以生成一颗继承树
Linux 进程间通信
Linux 进程间的通信机制通常被称为 Internel-Process communication,IPC
下面我们来说一说 Linux 进程间通信的机制,大致来说,Linux 进程间的通信机制可以分为 6 种
下面我们分别对其进行概述
信号 signal
信号是 UNIX 系统最先开始使用的进程间通信机制,因为 Linux 是继承于 UNIX 的,所以 Linux 也支持信号机制,通过向一个或多个进程发送异步事件信号
来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。
你可以在 Linux 系统上输入 kill -l
来列出系统使用的信号,下面是我提供的一些信号
进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:SIGSTOP
和 SIGKILL
信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。如果选择交给内核进行处理,那么就执行默认处理。
操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。
例如:当进程收到 SIGFPE
浮点异常的信号后,默认操作是对其进行 dump(转储)
和退出。信号没有优先级的说法。如果同时为某个进程产生了两个信号,则可以将它们呈现给进程或者以任意的顺序进行处理。
下面我们就来看一下这些信号是干什么用的
- SIGABRT 和 SIGIOT
SIGABRT 和 SIGIOT 信号发送给进程,告诉其进行终止,这个 信号通常在调用 C标准库的abort()
函数时由进程本身启动
- SIGALRM 、 SIGVTALRM、SIGPROF
当设置的时钟功能超时时会将 SIGALRM 、 SIGVTALRM、SIGPROF 发送给进程。当实际时间或时钟时间超时时,发送 SIGALRM。当进程使用的 CPU 时间超时时,将发送 SIGVTALRM。当进程和系统代表进程使用的CPU 时间超时时,将发送 SIGPROF。
- SIGBUS
SIGBUS 将造成总线中断
错误时发送给进程
- SIGCHLD
当子进程终止、被中断或者被中断恢复,将 SIGCHLD 发送给进程。此信号的一种常见用法是指示操作系统在子进程终止后清除其使用的资源。
- SIGCONT
SIGCONT 信号指示操作系统继续执行先前由 SIGSTOP 或 SIGTSTP 信号暂停的进程。该信号的一个重要用途是在 Unix shell 中的作业控制中。
- SIGFPE
SIGFPE 信号在执行错误的算术运算(例如除以零)时将被发送到进程。
- SIGUP
当 SIGUP 信号控制的终端关闭时,会发送给进程。许多守护程序将重新加载其配置文件并重新打开其日志文件,而不是在收到此信号时退出。
- SIGILL
SIGILL 信号在尝试执行非法、格式错误、未知或者特权指令时发出
- SIGINT
当用户希望中断进程时,操作系统会向进程发送 SIGINT 信号。用户输入 ctrl - c 就是希望中断进程。
- SIGKILL
SIGKILL 信号发送到进程以使其马上进行终止。与 SIGTERM 和 SIGINT 相比,这个信号无法捕获和忽略执行,并且进程在接收到此信号后无法执行任何清理操作,下面是一些例外情况
僵尸进程无法杀死,因为僵尸进程已经死了,它在等待父进程对其进行捕获
处于阻塞状态的进程只有再次唤醒后才会被 kill 掉
init
进程是 Linux 的初始化进程,这个进程会忽略任何信号。
SIGKILL 通常是作为最后杀死进程的信号、它通常作用于 SIGTERM 没有响应时发送给进程。
- SIGPIPE
SIGPIPE 尝试写入进程管道时发现管道未连接无法写入时发送到进程
- SIGPOLL
当在明确监视的文件描述符上发生事件时,将发送 SIGPOLL 信号。
- SIGRTMIN 至 SIGRTMAX
SIGRTMIN 至 SIGRTMAX 是实时信号
- SIGQUIT
当用户请求退出进程并执行核心转储时,SIGQUIT 信号将由其控制终端发送给进程。
- SIGSEGV
当 SIGSEGV 信号做出无效的虚拟内存引用或分段错误时,即在执行分段违规时,将其发送到进程。
- SIGSTOP
SIGSTOP 指示操作系统终止以便以后进行恢复时
- SIGSYS
当 SIGSYS 信号将错误参数传递给系统调用时,该信号将发送到进程。
- SIGTERM
我们上面简单提到过了 SIGTERM 这个名词,这个信号发送给进程以请求终止。与 SIGKILL 信号不同,该信号可以被过程捕获或忽略。这允许进程执行良好的终止,从而释放资源并在适当时保存状态。SIGINT 与SIGTERM 几乎相同。
- SIGTSIP
SIGTSTP 信号由其控制终端发送到进程,以请求终端停止。
- SIGTTIN 和 SIGTTOU
当 SIGTTIN 和SIGTTOU 信号分别在后台尝试从 tty 读取或写入时,信号将发送到该进程。
- SIGTRAP
在发生异常或者 trap 时,将 SIGTRAP 信号发送到进程
- SIGURG
当套接字具有可读取的紧急或带外数据时,将 SIGURG 信号发送到进程。
- SIGUSR1 和 SIGUSR2
SIGUSR1 和 SIGUSR2 信号被发送到进程以指示用户定义的条件。
- SIGXCPU
当 SIGXCPU 信号耗尽 CPU 的时间超过某个用户可设置的预定值时,将其发送到进程
- SIGXFSZ
当 SIGXFSZ 信号增长超过最大允许大小的文件时,该信号将发送到该进程。
- SIGWINCH
SIGWINCH 信号在其控制终端更改其大小(窗口更改)时发送给进程。