进程管理(创建进程)
在Linux中,首先要创建一个进程。创建进程的系统调用叫fork
。创建一个新的进程调要用fork
来实现,其中老的进程叫 父进程,新的进程叫 子进程。
当父进程调用fork
创建进程的时候,子进程将父进程的数据全部拷贝了一份。当然,如果不进行特殊处理,父进程和子进程都按相同的程序代码进行下去,这样就没有意义了。
所以,我们往往会这样处理,对于fork
系统调用的返回值,如果当前进程是子进程就返回0;如果当前进程是父进程就返回子进程的进程号。这样就首先有了一个区分。接下来我们就来判断,如果是父进程,我们就做原来的事情,而如果是子进程,我们需要请求另一个系统调用execve
来执行另一个程序。
有时候,父进程要关心子进程的运行情况。有个系统调用waitpid
,父进程可以调用它,将子进程的进程号作为参数传给它,父进程就可以知道子进程有没有运行完了。
内存管理
在操作系统中,每个进程都有自己的内存,互相之间不干扰,有独立的 进程内存空间。内存空间中,放程序代码的这部分,我们称为 代码段。放进程运行中产生数据的这部分,我们称为 数据段。其中局部变量的部分,在当前函数执行的时候起作用。当进入另一个函数时,这个变量就释放了;也有动态分配的,会较长时间保存,指明才会被销毁,这部分被称为 堆。
在这里介绍两个堆里面分配内存的系统调用,brk
和mmap
。当分配的内存数量比较小的时候,使用brk
,会和原来的堆的数据连在一起。当分配内存数量比较大时,使用mmap
,会重新划分一块区域。
文件管理
对于文件的操作,下面有六个系统调用是最重要的:
- 对于已有的文件,可以使用
open
打开这个文件,close
关闭这个文件 - 对于没有的文件,可以使用
creat
创建文件 - 打开文件后,可以使用
lseek
跳到文件的某个位置 - 可以对文件的内容进行读写,分别是
read
和write
。
Linux有个特点,一切皆文件:
- 启动一个进程,需要一个程序文件,它是一个 二进制文件。
- 启动的时候,需要加载一些配置文件,例如yml、properties等,这是文本文件;启动之后会打印一些日志,如果写到硬盘上,也是 文本文件。
- 如果我们把日志打印到交互控制台上,在命令行打印出来的其实也是文件,是标准输出 stdout文件。
- 这个进程的输出可以作为另一个进程的输入,这种方式称为 管道,管道也是文件。
- 进程可以通过网络和其他进程进行通信,建立的 Socket,也是一个文件。
- 进程需要访问外部设备,设备也是一个文件。
- 文件被存储在文件夹里面,所以 文件夹也是一个文件。
- 进程运行起来,想要看到进程运行的情况,会在/proc下面有对应的进程号,还是一系列文件。
每个文件,Linux都会分配一个 文件描述符,这是一个整数。有了这个文件描述符,我们就可以使用系统调用,查看或者干预进程运行的方方面面。
信号处理
当程序遇到异常情况时,例如中断,这是就需要向系统发送一个 信号(Signal)。经常遇到的信号有以下几种:
- 执行一个程序的时候,在键盘里输入“Ctrl + C”
- 非法访问内存
- 硬件故障
- 用户进程通过
kill
函数,将一个用户信号发送给另一个进程。
对于不严重的信息,可以忽略。可是对于类似SIGKILL
(终止某个进程的信号)和SIGSTOP
(中止某个进程的信号)是不能忽略的,可以执行对该信号默认的动作。每种信号都定义了默认的动作,例如硬件故障,默认终止;也可以提供信号处理函数,可以通过sigaction
系统调用,注册一个信号处理函数。
进程间通信
当某个程序比较大的时候,可能有多个进程,不同的进程需要相互交流,配合才能完成,这就需要一个进程之间的沟通机制。进程之间的沟通方式有很多种,我们一个一个看。
首先就是发个消息,不需要一段很长的数据,这种方式称为 消息队列。由于一个程序的多个进程进行信息交互时,这个消息队列是在内核里的,我们可以通过msgget
创建一个新的队列,msgsnd
将消息发送到消息队列,而消息接收方可以使用msgrcv
从队列种取消信息。
当两个进程需要交互的信息比较大的时候,可以使用 共享内存的方式。这时候,我们可以通过shmget
创建一个共享内存块,通过shmat
将共享内存映射到自己的内存空间,然后就可以读写了。
但是,两个进程访问共享内存块中的数据就会存在“竞争”的关系。这就需要有一种方式,让不同的进程能够排他的访问,这就是信号量的机制 Semaphore.
关于这个机制,我们说一种简单的场景。
对于只允许一个人访问的需求,我们可以将信号量设置为1。当一个人访问的时候,先调用sem_wait
。如果这时候没有人访问,则占用这个信号量。如果这个时候另一个人要访问也会调用sem_wait
。由于之前的一个人访问过了,所以后面来的必须要等待上一个访问完才能访问。当上一个人访问完了会调用sem_post
将信号量释放,于是下一个人等待结束,可以访问这个资源了。
网络沟通
如果一台Linux要和另一台Linux交流,这时候,我们就需要用到网络服务了。
不同的机器通过网络互相通信,它们需要遵循相同网络协议,TCP/IP网络协议栈。Linux内核里有对于网络协议栈的实现。那如何暴露出服务给进程使用呢?
网络服务是通过套接字Socket来提供服务的。Socket的中文意思可以译作“插口”,我们就可以想象,使用这个Socket将两个系统建立联系。
我们可以通过Socket
系统调用建立一个Socket。Socket也是一个文件,也有一个文件描述符,也可以通过读写函数进行通信。
查看源代码中的系统调用
对于64位的操作系统,在源码中找到unistd_64.h文件,里面就有对于系统调用的定义。
Glibc
Glibc是Linux下使用的开源标准C库,它是GNU发布的libc库。Glibc为程序员提供丰富的API,除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。
每个特定的系统调用对应了至少一个Glibc封装的库函数,比如系统提供的打开文件系统调用sys_open
对应的是Glibc中的open
函数。
有时候,Glibc一个单独的API可能调用多个系统调用,比如说,Glibc提供的printf函数就会调用如sys_open
、sys_mmap
、sys_write
、sys_close
等等系统调用。
也有时候,多个API会对应同一个系统调用,如Glibc下实现的malloc
、calloc
、free
等函数来分配和释放内存,都利用了内核的sys_brk
的系统调用。
有问题?发送 issues 给我~