1946年美籍匈牙利科学家冯·诺依曼提出存储程序原理,把程序本身当做数据来对待,程序和该程序处理的数据用同样的方式存储,并确定了存储程序计算机的五大组成部分和基本工作方法。
半个多世纪以来,计算机制造技术发生了巨大变化,但冯·诺依曼体系结构仍然沿用至今,人们将冯·诺依曼称为计算机界的祖师爷。
注意:
如今,我们的计算机都是使用冯·诺依曼体系结构,包含以下组成部分:
下面我们使用一个QQ聊天中数据的流动过程来理解冯·诺依曼体系结构:
当我们使用QQ和好友聊天时,键盘是输入设备,输入设备将信息送入内存,CPU从内存中取出该数据进行处理,处理完毕后放回内存,然后处理后的数据通过网卡发送出去,这里网卡就是输出设备。
好友接收消息,网卡接收到数据,将数据送入内存。这里网卡是输入设备。CPU从内存中取出该数据进行处理,处理完毕后放回内存,然后处理后的数据通过显示器进行显示,这里显示器就是输出设备。
操作系统(Operating System,简称OS)是管理计算机硬件与软件资源的计算机程序(一款纯正的“搞管理”的软件),同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供了一个让用户与系统交互的操作界面。
笼统的理解,操作系统包含:
设计操作系统的目的:
计算机如何管理硬件:
系统调用与库函数:
我们知道操作系统管理硬件资源是先组织再描述,那么对于运行中的程序的管理呢?
同样的,操作系统对运行中程序的管理,同样是先描述,再组织。
简单来说,进程就是运行中的程序。
课本上概念:程序的一个执行示例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
描述:
组织:
我们先跑起来一个进程。
#include
#include
int main(){
while(1){
// 休眠1s
sleep(1);
}
return 0;
}
ps -aux | grep pcb | grep -v grep
可以看到其进程标识符为11369。
我们再使用ls命令来查看一下。
ls /proc/11369
上述可以看到,使用ps命令可以查看进程标识符,除了ps命令外,还可以使用系统调用来查看进程标识符。
pid_t getpid(void);
返回值:
调用进程的进程标识符。
pid_t getppid(void);
返回值:
调用进程的父进程的进程标识符。
#include
#include
int main(){
while(1){
std::cout << "pid: " << getpid() << std::endl;
std::cout << "ppid: " << getppid() << std::endl;
// 休眠2s
sleep(2);
}
return 0;
}
我们来看一下内核源码中怎么说?
[sss@aliyun ~]$ vim /usr/src/kernels/3.10.0-957.5.1.el7.x86_64/include/linux/sched.h
/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_PARKED 512
#define TASK_STATE_MAX 1024
我们来创建一个僵死进程的示例:
#include
#include
#include
int main(){
// 创建一个子进程
pid_t pid = fork();
if(pid < 0){
perror("fork");
return 1;
} else if(pid > 0){
// 父进程
printf("parent[%d] is sleeping...\n", getpid());
// 睡眠60s
sleep(120);
} else{
// 子进程
printf("child[%d] is begin Z...", getpid());
// 睡眠5s
sleep(60);
return 0;
}
return 0;
}
僵尸进程如何处理呢?
如何避免产生僵尸进程呢?
父进程先退出,子进程就称之为“孤儿进程”。孤儿进程被1号init进程领养。
下面来看一段代码演示:
#include
#include
#include
int main(){
// 进程创建
pid_t pid = fork();
if(pid < 0){
perror("fork");
return 1;
} else if(pid == 0){
// 子进程
printf("Child Process: %d\n", getpid());
// 进程休眠10s
sleep(10);
} else{
// 父进程
printf("Parent Process: %d\n", getpid());
// 进程休眠3s
sleep(3);
return 0;
}
return 0;
}
[sss@aliyun ~]$ while :; do ps -ef | grep orphan | grep -v grep; sleep 1; echo "--------------------------------------------------------"; done
其他概念:
注意: 进程的nice值不是进程的优先级,它们不是一个概念,但是进程的nice值会影响到进程的优先级变化,可以将nice值理解为进程优先级的修正数据。
可以使用top命令查看进程的优先级。
使用top命令更改已经存在的进程的nice值:
#include
#include
#include
int main(){
int val = 10;
pid_t pid = fork();
if(pid < 0){
// 出错
perror("fork error");
}
else if(pid == 0){
// 子进程
printf("child: address-> %p, value-> %d\n", &val, val);
}
else{
// 父进程
printf("parent: address-> %p, value-> %d\n", &val, val);
}
sleep(1);
return 0;
}
可以发现输出的结果一模一样,很好理解,因为子进程是父进程的拷贝,所以数据应该是一样的,下面我们将代码改动一下:
#include
#include
#include
int main(){
int val = 10;
pid_t pid = fork();
if(pid < 0){
// 出错
perror("fork error");
}
else if(pid == 0){
// 子进程
val = 20;
printf("child: address-> %p, value-> %d\n", &val, val);
}
else{
// 父进程
printf("parent: address-> %p, value-> %d\n", &val, val);
}
sleep(1);
return 0;
}
我们发现,父子进程,输出地址是一样的,但是变量的内容不一样。这是为什么呢?
首先变量内容不一样,所以父子进程输出的变量不是同一个变量。但是地址是一样的,这是因为我们看到的是虚拟地址。我们在C/C++语言中看到的地址,全都是虚拟地址,物理地址,用户一概看不到,由OS统一管理。进程地址空间由一个结构体mm_struct来描述。
由上图可以看出,同一个变量地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址上!
页表: 记录虚拟地址和物理地址之间的映射关系;并且对虚拟地址进行访问控制。
程序地址空间优点: 内存充分利用,内存访问控制,保持进程独立性。
什么是环境变量?
常见环境变量:
查看环境变量的方法:
# NAME:环境变量名称
[sss@aliyun ~]$ echo $NAME
下面我们来看一下环境变量的用处:
首先来看一下PATH环境变量:
首先我们来写一个程序:
#include
int main(){
std::cout << "hello, world!" << std::endl;
return 0;
}
然后生成可执行程序:
然后执行./hello和hello对比二者的区别:
我们可以看到,./hello可以执行,hello直接执行会出现命令找不到,为什么Linux下的命令可以直接执行,而我们生成的二进制程序不能直接执行,必须要加上路径才可以执行,这是为什么呢?
下面我们将我们生成的程序所在路径添加到PATH中再来直接运行hello:
[sss@aliyun pcb]$ export PATH=$PATH:/home/sss/prictice/linux/pcb
从上面可以看出,将生成的hello程序所在路径添加到PATH环境变量中后,就可以直接运行hello程序,而不用带路径了,这说明,那些直接运行不需要带路径的命令,都是通过PATH环境变量实现的。
下面我们再来看一下HOME环境变量:
首先我们切换到root用户,查看一下HOME环境变量。
[sss@aliyun pcb]$ su
Password:
[root@aliyun pcb]# echo $HOME
[root@aliyun pcb]# exit
exit
[sss@aliyun pcb]$ echo $HOME
和环境变量相关的命令:
环境变量的组织方式:
每个程序都会收到一张环境表,环境表中是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
通过代码获取环境变量:
#include
int main(int argc, char* argv[], char* env[]){
// 循环打印所有环境变量
for(int i = 0; env[i] != nullptr; ++i){
std::cout << env[i] << std::endl;
}
return 0;
}
#include
int main(){
// 第三方环境变量引入
extern char** environ;
// 循环打印所有环境变量
for(int i = 0; environ[i]; ++i){
std::cout << environ[i] << std::endl;
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
char *getenv(const char *name);
参数:
name:环境变量名。
返回值:
存在,则返回环境变量内容,不存在返回NULL。
#include
#include
int main(){
// 查看环境变量HOME的内容
std::cout << getenv("HOME") << std::endl;
return 0;
}