目录
冯诺依曼体系结构
操作系统(Operator System)
进程
基本概念
组织进程
查看进程
进程状态
僵尸进程危害
环境变量
程序地址空间
挂起
进程创建
写时拷贝
进程终止
_exit函数
exit函数
参数:
冯诺依曼体系结构
关于冯诺依曼,必须强调几点:
这里的存储器指的是内存不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。一句话,所有设备都只能直接和内存打交道。
操作系统(Operator System)
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程
基本概念
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为 PCB ( process control block ), Linux 操作系统下的 PCB 是 : task_struct
在 Linux 中描述进程的结构体叫做 task_struct。task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM( 内存 ) 里并且包含着进程的信息。
标示符: 描述本进程的唯一标示符,用来区别其他进程。状态: 任务状态,退出代码,退出信号等。优先级: 相对于其他进程的优先级。程序计数器: 程序中即将被执行的下一条指令的地址。内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
组织进程
查看进程
大多数进程信息同样可以使用top和ps这些用户级工具来获取
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
-
父进程调用fork,返回子线程pid(>0)
-
子进程调用fork,子进程返回0,调用失败的话就返回-1
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret == 0){ //child
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}else{ //father
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
进程状态
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1; }
else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}else{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!内存泄漏?是的!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
UID : 代表执行者的身份PID : 代表这个进程的代号PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号PRI :代表这个进程可被执行的优先级,其值越小越早被执行NI :代表这个进程的nice值
环境变量
程序地址空间
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出
// 与环境相关,观察现象即可parent[2995]: 0 : 0x80497d8child[2996]: 0 : 0x80497d8
我们发现,输出出来的变量值和地址是一模一样的,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动 :
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
// 与环境相关,观察现象即可child[3046]: 100 : 0x80497e8parent[3045]: 0 : 0x80497e8
变量内容不一样 , 所以父子进程输出的变量绝对不是同一个变量但地址值是一样的,说明,该地址绝对不是物理地址!在 Linux 地址下,这种地址叫做 虚拟地址我们在用 C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由 OS 统一管理OS 必须负责将 虚拟地址 转化成 物理地址 。进程地址空间所以之前说 ‘ 程序的地址空间 ’ 是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
进程地址空间
- 使用物理地址不安全,所以使用虚拟地址
- 每一个进程有自己的pcb
- 操作系统给每一个进程创造虚拟地址空间
深入理解虚拟地址
- 虚拟地址会通过映射机制来访问实际的物理内存物理地址(页表)
- 地址空间不要仅仅理解为是os内部要遵守的,其实编译器也要遵守,即编译器编译代码的时候,就已经给我们形成了,各个区域,并且采用和linux内核一样的编址方式,给每一个变量,每一行代码都进行了编址,所以程序在编译的时候,每一个字段早已经具有了一个虚拟地址
- Cpu拿到的都是虚拟地址,通过页表跳转读取到物理地址的内容
- 本质上,因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,而并不是物理内存
- 只有当你真正的通过虚拟地址传递到cpu再访问物理地址空间的时候(是由操作系统自动完成,用户包括进程完全没有感知),才会执行内存相关的管理算法,帮你申请内存,构建页表映射关系,然后在进行内存的访问
因为在物理内存中理论上可以任意位置加载,那么是不是物理内存的所有数据和代码都是乱序的?
没错,但是因为有页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,
那么是不是在进程视角所有内存分布都是有序的?
地址空间+页表的存在,可以将内存分布有序化。
进程要访问的物理内存中的数据和代码,可能并没有在物理内存中,同样也可以让不同的进程映射到不同的物理内存,很容易让进程独立性的实现
进程的独立性可以通过地址空间+页表的方式实现。
因为有地址空间的存在,每一个进程都认为自己拥有4gb的空间,并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。
所以每一个进程是不需要知道其他进程的存在的。·
加载本质是创建进程,那么是不是必须非要把所有程序的代码和数据加载到内存中,并且创建内核数据结构建立映射关系?
在最极端的情况下,甚至只有内核结构被创建出来了。
理论上可以实现对程序的分批加载
挂起
进程的数据和代码被换出了,就叫被挂起
创建子进程,不需要将不会被访问的或者只会读取的数据拷贝一份
只有将来会被父或者子进程写入的数据,值得被拷贝。一般来说即便是os,也无法提前知道哪些空间可能会被写入,所以os选择写时拷贝技术,来进行将父子进程的数据分离。
进程终止时,操作系统做了什么
要释放进程申请的相关内核数据结构和对应的数据和代码。本质就是释放系统资源
进程创建
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中fork 返回,开始调度器调度
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
子进程返回0,父进程返回的是子进程的pid
写时拷贝
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
进程终止
代码运行完毕,结果正确代码运行完毕,结果不正确代码异常终止
正常终止(可以通过 echo $? 查看进程退出码):1. 从 main 返回2. 调用 exit3. _exit
_exit函数
#include <unistd.h> void _exit(int status); 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然 status 是 int ,但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 $?发现返回值是 255 。exit函数
#include <unistd.h> void exit(int status);
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
1. 执行用户通过 atexit 或 on_exit 定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写入3. 调用_exit
int main()
{
printf("hello");
exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
printf("hello");
_exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#
return退出
进程等待
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
#include<sys/types.h>#include<sys/wait.h>pid_t wait(int*status);
- 成功返回被等待进程pid,失败返回-1。
pid_ t waitpid(pid_t pid, int *status, int options);返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
如果子进程已经退出,调用 wait/waitpid 时, wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。如果在任意时刻调用 wait/waitpid ,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。
测试代码:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main( void )
{
pid_t pid;
if ( (pid=fork()) == -1 )
perror("fork"),exit(1);
if ( pid == 0 ){
sleep(20);
exit(10);
} else {
int st;
int ret = wait(&st);
if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
printf("child exit code:%d\n", (st>>8)&0XFF);
} else if( ret > 0 ) { // 异常退出
printf("sig code : %d\n", st&0X7F );
}
}
}
测试结果:
[root@localhost linux]# ./a.out #等20秒退出
child exit code:10
[root@localhost linux]# ./a.out #在其他终端kill掉
sig code : 9
具体代码实现
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
} else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
} else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
} else{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}