概念:操作系统是管理计算机硬件与软件资源的计算机程序,简称OS。
为什么要有操作系统:
1.给用户提供稳定、高效和安全的运行环境,为程序员提供各种基本功能(OS不信任任何用户,不让用户或者程序员直接与硬件进行交互)。
2.管理好各种软硬件资源。
从这张图我们可以看到几点内容:
- OS管理的硬件部分: 网卡、硬盘等
- OS管理的软件部分: 内存管理、驱动管理、进程管理和文件管理,还有驱动和系统调用接口
进程
进程和程序的概念
我们平时所写的C语言代码,通过编译器的编译,最终会成为一个可执行的程序,当这个可执行程序运行起来之后,它就变成了一个进程。
程序是存放在存储介质(程序平时都存放在磁盘当中)上的一个可执行文件,而进程就是程序执行的过程。进程的状态是变化的,其中包括进程的创建、调度和死亡。程序是静态的,进程是动态的。
进程: 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
如何描述进程:
- 进程的所有属性信息都被放在一个叫做进程控制块的结构体中,可以理解为进程属性的集合。
- 这个数据结构的英文名称是PCB(process control block),在Linux的OS下的PCB是task_struct(Linux内核中的一种数据结构,它会被装载到RAM(内存)中并且包含并包含进程的信息)。
task_struct内容有哪些?
-
标识符:描述本进程的唯一标识符(就像是我们每个人的身份证)。
-
状态:任务状态、退出代码、退出信号等。
-
优先级: 程序被CPU执行的顺序(后面会单独介绍)。
-
程序计数器: 一个寄存器中存放了一个pc指针,这个指针永远指向即将被执行的下一条指令的地址。
-
内存指针: 包含程序代码和进程相关的数据的指针,还有和其它进程共享的内存快的指针。这样就可以PCB找到进程的实体。
-
上下文数据: 在单核CPU中,进程需要在运行队列(run_queue) 中排队,等待CPU调度,每个进程在CPU中执行时间是在一个时间片内的,时间片到了,就要从CPU上下来,继续去运行队列中排队。
-
I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
-
记账信息: 能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
组织进程
在内核源代码中发现,所有运行在系统里的进程都以task_struct链表形式存在内核中。
进程的状态
进程的状态反应进程执行过程的变化。这些状态随着进程的执行和外界的变化而转换。
五态模型中,进程分为新建态,终止态,运行态,就绪态,就绪态。
(1)TASK_RUNNING(运行态):进程正在被CPU执行。当一个进程被创建的时候会处于TASK_RUNNABLE,表示已经准备就绪,正在准备被调度。
(2)TASK_INTERRUPTIBLE(可中断状态):进程正在睡眠(阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置成运行态。处于此状态的进程也会因为接收到信号而提前被唤醒,比如给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将会被先唤醒(进入TASK_RUNNABLE状态),然后再响应SIGKILL信号而退出(变为TASK_ZOMBIE状态),并不会从TASK_INTERRUPTIBLE状态直接退出。
(3)TASK_UNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源被满足的时候被唤醒,但是不可以由其他进程通过信号或者中断唤醒。由于不接受外来的任何信号,因此无法用KILL杀掉这些处于该状态的进程。而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。
(4)TASK_ZOMBIE(僵死):表示进程已经结束,但是其父进程还没有回收子进程的资源。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用wait函数释放子进程的资源,子进程的进程描述符就会被释放。
(5)TASK_STOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE状态。
下面是进程状态在源码中的定义:
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
查看进程状态相关的命令:
ps命令可以查看进程详细的状态,常用选项如下:
选项 | 含义 |
---|---|
-a | 显示终端上的所有进程,包括其他进程 |
-u | 显示进程的详细状态 |
-x | 显示没有控制终端的进程 |
-w | 显示加宽,以便显示更多的信息 |
-r | 只显示正在运行的进程 |
PID就是进程的进程号,STAT是进程此时处于什么状态。
有下面两种命令(前者查看所用进程的名字,后者可以查看进程的父子关系):
ps aux/ps axj
进程号和相关函数
每个进程都有一个进程号来标识,其类型为pid_t(整型)。进程号是唯一的,但是进程号是可以重用的。当一个进程终止后,其进程号可以再次使用。
进程号(PID)
getpid()可以获取当前进程的进程号。
父进程号(PPID)
getppid()可以获取当前进程的父进程号
进程组号(PGID)
getpgid()可以获取当前进程进程组号
进程创建
fork函数(系统调用)
pid_t fork(void);
功能:通过复制当前进程,为当前进程创建一个子进程
返回值:成功:子进程中返回0,父进程中返回子进程的pid_t。
失败:返回-1。
进程调用fork函数,内核需要做什么?
- 给子进程分配内存空间,并为子进程创建PCB
- 将父进程部分数据结构内容(还有代码和数据暂时共享)拷贝至子进程
- 添加子进程到系统进程列表(运行队列)当中
- fork返回,开始CPU调度器调度
fork之后执行什么?
父子进程共享一份代码,fork之后,一起执行fork之后的代码,且二者之间是独立的,不会相互影响。
父进程绝大部门东西都被子进程继承,代码也是,但是在执行的过程中,父进程的PCB中存在一个pc指针,记录着下一条指定的地址,当父进程执行到fork的时候,pc指针也只想fork的下一条指令,子进程也继承了pc指针的虚拟地址,本来子进程全部继承了父亲的共享代码,但是此时pc也是指向fork的下一条指令,所以父子进程都从fork之后开始执行。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子进程
{
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
else if (ret > 0)// 父进程
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
sleep(1);
return 0;
}
父子进程关系
使用fork函数得到的子进程是父进程的一个复制品,每个进程都有自己的进程控制块PCB,再这个PCB中子进程从父进程中继承了整个进程的地址空间:包括进程上下文,进程堆栈,打开的文件描述符,信息控制设定,进程优先级,进程组号等等,但是进程的地址空间都是虚拟空间,子进程PCB继承的都是虚拟地址。
写时拷贝
通常情况下,父子进程共享一份代码,并且数据都是共享的,当任意一方试图写入更改数据的时候,那么这一份便要以写时拷贝的方式各自私有一份副本。
从图中可以看出,发生写时拷贝后,修改方将改变页表中对该份数据的映射关系,父子进程各自私有那一份数据,且权限由只读变成了只写,虚拟地址没有改变,改变的是物理内存页的物理地址。(涉及到虚拟地址,可以看我上面发的文章)
问题思考:
1.为什么代码要共享?
代码是不可以被修改的,所以各自私有很浪费空间,大多数情况下是共享的,但要注意的是,代码在特殊情况下也是会发生写时拷贝的,也就是进程的程序替换(后面会单独介绍)。
2.写实拷贝的作用?
- 可以减少空间的浪费,在双方都不对数据或代码进行修改的情况下,各自私有一根数据和代码是浪费空间的。
- 维护进程之间的独立性,虽然父子进程共享一份数据,但是父子中有一方对数据进行修改,那么久拷贝该份数据到给修改方,改变修改方中页表对这份数据的映射关系,然后对数据进行修改,这样不管哪一方对数据进行修改都不会影响另一方,这样就做到了独立性。
3.写时拷贝是对所有数据进行拷贝吗?
答案是否定的。如果没有修改的数据进行拷贝,那么这样还是会造成空间浪费的,没有被修改的数据还是可以共享的,我们只需要将修改的那份数据进行写时拷贝即可。
理论还是太枯燥,上代码!
代码1:栈区局部变量
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int var = 88;
//创建一个子进程
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子进程
{
sleep(1);
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
printf("子进程睡醒之后 var = %d\n",var);
}
else if (ret > 0)// 父进程
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
printf("父进程之前 var =%d\n", var);
var++;
printf("父进程之后 var =%d\n", var);
}
sleep(1);
return 0;
}
运行结果:
读时共享,写时拷贝。这里的父进程一开始时共享var的数据给子进程,但是此时子进程睡了一秒,就执行父进程,父进程中var的值被改变,此时写时拷贝,var会拷贝一份到子进程当中,所以父进程修改var的值不会影响到子进程中var的值。这里的局部变量在栈区。
代码2:全局变量
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int var = 88;
int main()
{
//创建一个子进程
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)// 子进程
{
sleep(1);
printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
printf("子进程睡醒之后 var = %d\n",var);
}
else if (ret > 0)// 父进程
{
printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
printf("父进程之前 var =%d\n", var);
var++;
printf("父进程之后 var =%d\n", var);
}
sleep(1);
return 0;
}
运行结果:
子进程var值也不会受到影响,遵循读时共享,写时拷贝的原则。
总结:
-
父子进程由独立的数据段、堆、栈、共享代码段(每个进程都有属于自己的PCB)。
-
Linux中每个进程都有4G的虚拟地址空间(独立的3G用户空间和共享的1G内核空间),fork创建的子进程也不例外。
(1)1G内核空间既然是所有进程共享,因此fork创建的子进程自然也将有用;
(2)3G的用户空间是从父进程而来。
-
fork创建子进程时继承了父进程的数据段、代码段、栈、堆,值得注意的是父进程继承来的是虚拟地址空间,进程上下文,打开的文件描述符,信息控制设定,进程优先级,进程组号,同时也复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟空间,映射的物理内存也是一致的。(独立的虚拟地址空间,共享父进程的物理内存)。
-
由于父进程和子进程共享物理页面,内核将其标记为“只读”,父子双方均无法对其修改。无论父子进程尝试对共享的页面执行写操作,就产生一个错误,这时内核就把这个页复制到一个新的页面给这个进程,并把原来的只读页面标志为可写,留给另外一个进程使用----写时复制技术。
-
内核在子进程分配物理内存的时候,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程映射的时同一块物理内存。
进程终止
可以通过echo$?查看进程退出码
exit函数和return函数的区别
- main函数结束的时候也会隐式的调用exit函数。exit函数运行的时候首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有的输出流,关闭所有打开的流并且关闭通过标准IO函数创建的临时文件。
- exit时结束一个进程,他将删除进程使用的内存空间,同时把错误信息返回父进程;而return是返回函数值(return所在的函数框内)并且退出函数。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)。return也是如此,如图return在main函数中,那么结束的就是整个进程。return是函数的结束,exit是进程的结束。
- return是语言级别的,它表示了调用堆栈的返回;return( )是当前函数返回,当然如果是在主函数main, 自然也就结束当前进程了,如果不是,那就是退回上一层调用。在多个进程时。如果有时要检测上个进程是否正常退出。就要用到上个进程的返回值,依次类推。而exit是系统调用级别的,它表示了一个进程的结束。
- exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。
- 在main函数中exit(0)等价于return 0。
1.return函数返回退出码
main函数退出的时候,return的返回值就是进程的退出码。0在函数的设计中,一般代表是正确而非0就是错误。
2.调用exit函数
void exit(int status);
功能:结束当前正在执行的进程。
参数:返回给父进程的参数,根据需要填写。
在任意位置调用,都会使得进程退出,调用之后会执行执行用户通过 atexit或on_exit定义的清理函数,还会 关闭所有打开的流,所有的缓存数据均被写入。
int main()
{
cout << "12345";
sleep(3);
exit(0);// 退出进程前前会执行用户定义的清理函数,且刷新缓冲区
return 0;
}//输出12345
3.调用_exit函数
exit()和_exit()函数功能和用法都是一样的,但是区别就在于exit()函数是标准库函数,而__exit函数是系统调用。
在Linux的标准函数库中,有一套称做“高级I/O”的函数,我们熟知的printf(),fopen(),fread(),fwrite()都在此列,它们也被称作缓冲IO (buffered IO)",其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
- exit()作为库函数,封装的比较完善,exit将终止调用的进程,在退出程序之前,所有文件关闭,缓冲区刷新(输出内容),将刷新定义,并且调用所有已刷新的“出口函数”,在执行完清理工作之后,会调用_exit来终止进程。
- _exit()调用,但是不关闭文件,不刷新缓冲区,也不调用出口函数。
int main()
{
cout << "12345";
sleep(3);
_exit(0);// 直接退出进程,不刷新缓冲区
return 0;
}//不输出12345
4.异常终止
- ctrl+C终止前台进程
- kill发生9号信号杀死进程
进程等待
进程等待的必要性:
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等,这就是在执行exit时候执行的工作。但是仍然会保留一定的信息,这些信息主要指的是进程控制块PCB的信息(包括进程号,退出状态,运行事件等),而这些信息需要父进程调用wait或者waitpid函数得到他的退出状态同时彻底清理掉这个进程残留的信息。
- 子进程必须要比父进程先退出,否则会变成孤儿孤儿进程
- 父进程必须读取子进程的退出状态,回收子进程的资源。如果父进程不读取子进程退出状态,还不会释放子进程资源,那么子进程将处于僵死状态,会造成内存泄漏
- 父进程派给子进程的任务完成的如何,得知子进程执行结果
wait方法
*pid_wait(int status);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:status进程退出时候的状态。
返回值:成功:返回结束子进程的进程号。失败:-1.
注意以下几点:
- 调用wait会阻塞当前的进程,直到任意一个子进程退出或者收到一个不能忽视的信号才能被唤醒。
- 若调用进程没有子进程,该函数立刻返回;若它的子进程已经结束,该函数同样会立刻返回,并且会回收那个早已经结束进程的资源。
- 如果参数status的值不是NULL,wait就会把子进程退出时候的状态取出来并存入,这是一个整数值,指出了子进程是正常退出还是被非正常结束。
演示:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t ret= fork();
if (ret< 0){
cerr << "fork error" << endl;
}
else if (ret== 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
sleep(10);
pid_t id = wait(NULL);// 不关心子进程退出状态
printf("father finish waiting...\n");
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
//父进程再活5秒
sleep(5);
return 0;
}
由运行结果可以看出,父进程一只等待子进程结束,等待的时候子进程变成僵尸进程,等父进程彻底释放资源,子进程的状态由僵尸变成死亡状态。
waitpid方法
*pid_t waitpid(pid_t pid, int status , int options);
功能:等待子进程结束,如果子进程终止,此函数就会回收子进程资源。
参数:
pid:参数pid有以下几种类型:
pid>0 等待进程ID等于pid的子进程结束。
pid=0 等待同一个进程组中的任何子进程,如果子进程已经进入了别的进程组,waitpid不会等待它。
pid=-1 等待任意子进程,此时waitpid和wait的作用是一样的。
pid<-1 等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options:options提供了一些额外的选项来控制waitpid()
0:通wait(),阻塞父进程,等待子进程退出。
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID(可以进行基于阻塞等待的轮询访问)。
WUNTRACED:如果子进程暂停了此函数立马返回,并且不予理会子进程的结束状态(很少调用)。
返回值:
waitpid有三种情况:
(1)正常返回的时候,waitpid返回收集到的已回收子进程的进程的进程号。
(2)如果设置了WNOHANG,而调用中发现了没有已经退出的子进程可以等待,返回0。
(3)如果调用中出错,返回-1,此时errno会被设置成相应的值来指示错误所在。
代码示例:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t ret= fork();
if (ret< 0){
cerr << "fork error" << endl;
}
else if (ret== 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
sleep(10);
pid_t id = waitpid(-1, NULL, 0);// 不关心子进程退出状态,以阻塞方式等待
printf("father finish waiting...\n");
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
//父进程再活5秒
sleep(5);
return 0;
}
获取子进程的status
- wait和waitpid中都有一个status参数,该参数是一个输出型参数,由操作系统来填充
- 如果该参数给NULL,那么代表不关心子进程的退出信息
status的几种状态:(我们只研究status的低16位)
看图可以知道,低7位代表的是终止信号,第8位时core dump标志,高八位是进程退出码(只有正常退出是这个退出码才有意义)
status的0-6位和8-15位有不同的意义。我们要先读取低7位的内容,如果是0,说明进程正常退出,那就获取高8位的内容,也就是进程退出码;如果不是0,那就说明进程是异常退出,此时不需要获取高八位的内容,此时的退出码是没有意义的。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t ret = fork();
if (ret < 0){
cerr << "fork error" << endl;
}
else if (ret == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(1);
}
// parent
printf("father begins waiting...\n");
int status;
pid_t id = wait(&status);// 从status中获取子进程退出的状态信息
printf("father finish waiting...\n");
if (id > 0 && (status&0x7f) == 0){
// 正常退出
printf("child success exited, exit code is:%d\n", (status>>8)&0xff);
}
else if (id > 0){
// 异常退出
printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
}
else{
printf("father wait failed\n");
}
if (id > 0){
printf("child success exited\n");
} else{
printf("child exit failed\n");
}
return 0;
}
运行结果如下:
阻塞等待和非阻塞等待
操控者: 操作系统
阻塞的本质: 父进程从运行队列放入到了等待队列,也就是把父进程的PCB由R状态变成S状态,这段时间不可被CPU调度器调度
等待结束的本质: 父进程从等待队列放入到了运行队列,也就是把父进程的PCB由S状态变成R状态,可以由CPU调度器调度
阻塞等待: 父进程一直等待子进程退出,期间不干任何事情
示例:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0){
cerr << "fork error" << endl;
}
else if (id == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(0);
}
// 阻塞等待
// parent
printf("father begins waiting...\n");
int status;
pid_t ret = waitpid(id, &status, 0);
printf("father finish waiting...\n");
if (id > 0 && WIFEXITED(status)){
// 正常退出
printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
}
else if (id > 0){
// 异常退出
printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
}
else{
printf("father wait failed\n");
}
}
运行结果如下:
非阻塞等待: 父进程不断检测子进程的退出状态,期间会干其他事情(基于阻塞的轮询等待)
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0){
cerr << "fork error" << endl;
}
else if (id == 0){
// child
int count = 5;
while (count){
printf("child[%d]:I am running... count:%d\n", getpid(), count--);
sleep(1);
}
exit(0);
}
// 基于阻塞的轮询等待
// parent
while (1){
int status;
pid_t ret = waitpid(-1, &status, WNOHANG);
if (ret == 0){
// 子进程还未结束
printf("father is running...\n");
sleep(1);
}
else if (ret > 0){
// 子进程退出
if (WIFEXITED(status)){
// 正常退出
printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
}
else{
// 异常退出
printf("child exited error,exit singal is:%d", status&0x7f);
}
break;
}
else{
printf("wait child failed\n");
break;
}
}
}
运行结果如下: