操作系统
传统的计算机系统资源分为硬件资源和软件资源。硬件资源包括中央处理器,存储器,输入设备,输出设备等物理设备;软件资源是以文件形式保存在存储器上的成熟和数据等信息。
操作系统就是计算机系统资源的管理者。
如果你的计算机没有安装操作系统,那么你将面对的是0,1代码和一些难懂的机器指令,通过按钮或者按键来操作计算机,这样笨拙+费时。安装操作系统之后,你面对的就不再是笨拙的裸机,而是操作便利,服务周到的操作系统,从而明显的改善了用户界面。
操作系统为了保证自己的安全,也为了保证给用户能够提供服务,就以接口的方式给用户提供调用的入口,来获取操作系统内部的数据。
这个接口是操作系统提供的用C语言实现的,自己内部的函数调用---系统调用
所有访问操作系统的行为,都只能通过系统调用完成,因为操作系统不会让你随意的去对自己进行更改。
所以,操作系统就是一个管理者,利用操作系统能够有效的组织和管理系统中的各种软硬件资源,合理的组织计算机系统工作的流程,控制程序的执行,并且向用户提供一个良好的工作环境和友好的接口。(程序猿通过暴露出的接口,开发出来各种软件供普通用户使用)。
那么操作系统是如何管理的呢?
在学校中,我们就是最典型的被管理者,校长是管理者。管理者与被管理者是不需要见面的,查考勤情况的活肯定不是校长监督的吧。那么校长连A同学都不知道是谁,怎么管理好这么多同学呢?
其实只要校长拿到了学生的数据,就能进行管理,见不见面不是必须的,就算见面了,也是为了获取某同学的数据。管理的本质:是通过对数据的管理达到对人的管理。
但是不见面的情况下,校长是怎么知道A同学挂没挂科呢?这些数据都可以通过辅导员来拿到数据。
在这里,校长相当于操作系统,辅导员相当于驱动程序,学生相当于软硬件资源。所以操作系统要管理好软硬件资源是通过获取硬件的各种状态数据来进行管理,这个数据从驱动程序中获得。一个硬件不能用了,驱动程序把信息传递给操作系统,操作系统告知用户。
学校中的学生很多,他弄了一个excel表格,让辅导员按照这个表格获取学生的信息,辅导员获取完信息后,在把表格给校长,现在校长要找谁个子最高等信息,只需要遍历一遍表格即可。这个过程就是一个描述的过程。
但校长曾经是一个程序猿,他弄了一个结构体来实现这个表格。
struct student
{
char 学院[];
char 专业[];
......
struct student *next;
}
struct student stu1 = {};
每一个结构体对象里面存着学生的信息,通过next来对学生进行链接。
所以校长只要把这个学生链表管理好就行了。这样就将对学生的管理工作变成了对链表的增删查改。想找挂科超过3科的,直接遍历链表即可。填写学生信息的过程是描述过程,把学生通过节点链接起来的过程是组织的过程。
操作系统中,管理任何对象,最终就变成了对某种的数据结构的管理。操作系统管理的过程跟上面的例子一样:先描述在组织。
之前写通讯录之类管理系统,先把要存的信息写在结构体中,然后对这个结构体进行封装,这不就是先描述,在组织吗?
struct person
{
char name;
int age;
char telphone1;
char telphone2;
...
}
struct contact
{
struct person[100];
int num;
...
}
系统调用和库函数概念
操作系统不相信任何人,只会提供系统调用接口,如果你想简介的访问硬件或者打开文件之类的操作,是不能直接访问底层硬件,而是层层访问,以贯穿的形式。比如说printf函数,他是C标准函数,他的底层绝对要封装系统调用接口,所以C/C++封装的库函数,和系统调用接口的关系,是上下层被调用的关系,而不是直接绕过系统调用接口直接访问的。
那么谁在上,谁在下呢?
库函数在上,系统调用接口在下,
- 在开发的角度,操作系统对外表现为一个整体,但是会暴露一部分接口,供程序猿开发使用,这部分由操作系统提供的接口,叫做系统调用
- 系统调用在使用上, 功能比较基础,对用户的要求也相对较高,所以,有些开发者可以对部分系统调用进行适度封装,从而形成了库,有了库,就很利于上层用户或者开发者进行二次开发
进程
一个已经加载到内存中的程序,叫做进程。
这些已经打开的软件,都是进程,对上面的某一个进程,右键结束任务,就杀死了某个进程。
在Linux中通过ps ajx
可以查看所有的进程。
这些进程都有自己的名字和编号。
top
命令,查看正在运行的进程。
现在在Linux中写一段代码
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "This is a process." << std::endl;
sleep(1);
}
return 0;
}
在运行这段代码之后,输入指令查看进程。
一个名为proc的程序,是保存在磁盘当中的,在计算机开机的时候,操作系统一定是预先加载到内存中的。如果我想把proc运行,根据冯诺依曼结构,这个proc一定会先加载到内存中,是数据的部分,交给控制器;是二进制的部分交给运算器去执行。
但是一个操作系统可以同时运行多个进程,那么操作系统中可能会存在刚打开的进程,正在运行的进程,即将结束的进程,所以操作系统要将这些进程进行管理起来。要想管理,得先让操作系统认识这些进程,也就是先描述起来,然后对这些进程进程管理,也就是组织。
描述进程
任何一个进程在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程的结构体对象---PCB Process Ctrl Block(进程控制块)
那么PCB是什么呢??
要想认识某个事物,就要先知道它的属性,比如说,三边长度一样的封闭图形等。当属性堆积的够多的时候,这些属性集合起来,就是目标对象。
所以描述进程就是将最常用的属性放在一起。PCB就是进程属性的集合。
而进程就是PCB+程序和数据组成的。
PCB的内容
信息 | 含义 |
进程标识符 | 表明系统中的各个进程 |
状态 | 说明进程当前的状态 |
位置信息 | 指名程序及数据在主存或外存的物理位置 |
状态信息 | 参数,信号量,消息等 |
队列指针 | 链接同一状态的进程 |
优先级 | 进程调度的依据 |
现场保护区 | 将处理机的现场保护到该区域,以便再次调度时能继续正确运行 |
其他 | 因不同的系统而异 |
程序:程序部分描述了进程需要完成的功能。
数据:数据部分包括程序执行时所需的数据及工作区,该部分只能为一个进程所专用,是进程的可修改部分。
所以将程序加载到内存中,不光光是把代码和数据放到操作系统中,还会创建用来描述这个程序的PCB对象。
PCB中存在指针信息,用来找到自己的代码和数据。
系统当中不会只有一个进程,会有多个进程。这些进程,加载到内存中,在创建PCB对象,PCB中存在指针,通过指针再将所有的进程的PCB链接在一块,在通过PCB中的指针,找到自己的代码和数据。这样在操作系统中,对进程的管理,就变成了对单链表进行增删查改!
上面所描述的,是所有操作系统的原理,但是具体的操作系统会有差别。
那么Linux操作系统是怎么做的?
在Linux操作系统下的PCB是 tack_struct
tack_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_struct内容分类
跟上面的PCB的组成一样。
PCB -> tack_struct 结构体,里面包含进程的所有属性。
Linux中如何组织进程,Linux内核中,最基本的组织进程tack_struct的方式是采用双向链表组织的。
但这并不是存粹的双链表,可能task_struct中存在指针,指向一个队列,一棵树等其他数据结构中。Linux中数据结构的关系,一定是复杂的。
对tack_struct的管理,就是放在某个组织的数据结构中,这个进程要等待,放在等待队列里,要运行,放在运行队列里,所以这个进程要怎么工作,就放在哪一个在组织的数据结构中。
ps ajx | head -1 && ps ajx | grep ./proc
通过这个命令,就可以查看我们刚刚执行的那个名为proc的程序的进程
也可以通过 proc去查看。ls /proc
这个目录会在关机的时候,全部关闭,再开机的时候,再打开。
这些蓝色的都是目录,在这里都是以PID(唯一标识符)的形式表示的
./proc的PID是9965,既然蓝色的是目录,说明就可以进入到这个目录中查看一些信息。
当我ctrl+c结束这个进程的时候,再次查看这个进程,就会找不到。如果再次启动这个进程,PID就会发生变化。
第二个gerp.../proc的进程是grep ./proc的进程,因为进行过滤的时候系统创建出了这个进程。
这个exe文件是一个链接文件,指向正在运行的名为proc的进程。
cwd是当前进程的工作目录。
再使用 touch
命令的时候,直接再某一个目录上 touch
为什么能够直接创建出文件,而不用先输入路径呢?因为再使用 touch
命令的时候,系统自动创建进程,进程中的cwd会自动记录当前目录的路径。再创建的时候自动的把cwd拼接到要创建的文件名前面。 cwd/FileName
通过系统调用获取进程标识符
每个进程要被管理,就必须有一个唯一的标识符---PID
COMMAND代表进程执行的时候,是什么命令。
那么这个PID有什么命令呢?
现在我写了一个死循环
代码再不停的打印Hello World,我通过命令查看这个进程的PID
然后现在我要杀死这个进程
kill -9 PID
查到某个进程的PID,然后kill可以直接干掉。
如何查询自己的PID呢?
通过getpid函数可以.
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "I am " << getpid() << std::endl;
std::cout << "Parent am " << getppid() << std::endl;
}
return 0;
}
PID是自己本身的,PPID是父进程的PID。
这个程序我执行了两次,PID一直在变,PPID却没有变化,这个PPID是什么呢?
当我们执行程序的时候,bash会给我们创建进程。我们再命令行中输入的所有指令,都是bash进程的子进程。
通过系统调用创建进程-fork
如果我们自己想创建进程,fork可以完成
根据这个返回值来看,这个函数有两个返回值??
#include <unistd.h>
#include <stdio.h>
int main()
{
printf("begin:This is a process. pid:%d,ppid:%d",getpid(),getppid());
pid_t id = fork();
if (id == 0)
{
while (true)
{
printf("This is a child process. pid:%d,ppid:%d",getpid(),getppid());
sleep(1);
}
}
else if (id > 0)
{
while (true)
{
printf("This is a person process. pid:%d,ppid:%d",getpid(),getppid());
sleep(1);
}
}
else
{
printf("error");
}
else
{
std::cout << "error" << std::endl;
}
return 0;
}
按照我们以前写的程序,只要第一个if条件满足,就不会再执行其他的分支语句了。
这个代码再循环打印子进程和父进程中的内容,一份代码中跑了两个死循环,再之前是不可能实现的,但是有了fork就可以了,因为变成了两个进程--父进程和子进程。
父进程的pid是3870,子进程的ppid也是3870,说明这两个进程之间的关系是父子。
这个2592就是bash
说明父进程是通过bash来创建的。
上面的代码执行到fork的时候,创建出了子进程。
代码是从上往下执行的,执行到fork函数的时候,整个代码就会变成两个执行流,一个进入到fork大于0的代码中,一个进入到fork==0的代码中。这是能跑两个代码的原因。
为什么fork要给子进程返回0,给父进程返回子进程pid?
一般而言,fork之后的代码父子共享。返回不同的返回值,是为了区分,让不同的执行流,执行不同的代码块。
一个函数是如何做到返回两次的?
fork是一个函数,函数有自己的实现方法,它本身是在操作系统中,有自己的实现。
return也是代码,父进程执行到return的时候会返回一次,子进程执行到return的时候也会返回一次,所以就返回了两次。
fork有两个返回值,那么一个id变量怎么会有两个值呢?
父进程在执行的时候,会有自己的代码和数据,这个代码是和子进程共享的,数据呢?父进程有自己的数据的,子进程也应该有自己的数据。进程之间是有独立性的,一个进程崩了,不会影响另一个进程。所以子进程要想办法把父进程的数据拷贝一份,拷贝一份各有各的数据,但是子进程也有可能不会对一些数据进行访问,没用的数据也进行拷贝,会造成浪费。子进程要访问父进程中的数据,可以对要修改的数据,进行拷贝,改多少申请多少空间,这种技术是数据层面的写时拷贝。如果没有更改,父子进程的代码和数据共享。
上面说过,进程=PCB+代码和数据。创建子进程就是系统中多了一个进程。父进程中的PCB存在指针指向代码和数据,而子进程中也会存在指针,指向代码和数据,父子进程中的代码是共享的。fork之后,父子进程代码共享,但是可以做不同的事情。
fork之后,父子进程谁先运行呢?
这个是由调度器决定的。
进程状态
CPU只有一个的情况下,存在多个进程,这些进程要竞争CPU的资源,这些资源要合理的分配,所以CPU要维护一个运行队列(struct runqueue),这些进程要想运行,要先链接到运行队列当中,因为PCB本来就是数据结构对象,运行队列中的头指针指向PCB,尾指针指向最后一个PCB。这个时候CPU要运行进程,直接在运行队列中找到一个进程放到CPU中运行即可。
凡是处于运行队列中的进程,都属于运行状态
当一个进程在运行时,则该进程处于运行态
这个时候你在创建一个进程,这个进程想要执行,链接在队列中即可。
那么一个进程只要把自己放到CPU上开始运行了,是不是一直要执行完毕,才把自己放下来?
不是的,比如平常写代码的时候,我们写了一个死循环,但是其他程序还能正常运行。每一个进程都有一个叫做时间片的概念,一旦运行时间超过时间片,这个进程就会重新去排队,CPU运行新的进程,所以在一段时间内,所有进程代码都会被执行。也可以称为并发执行。
在操作系统中,底层存在各种各样的硬件。操作系统可以管理软硬件资源。这些硬件虽然都不同,但他们都可以用结构体对象来表示。
struct dev
{
int type;
int status;
struct task_struct *head;
....
}
将这些结构体在链接起来,这样就可用数据结构来管理这些硬件了。
现在我们写了一个带 std::cin
的程序,运行以后,设备要从键盘当中读取数据,但是这个时候我们不输入,此时这个进程就要等待从键盘上获取数据,那么这个队列就不能放在运行队列中,会放在等待队列(waitqueue)当中,直到从键盘上获取了数据。获取了数据之后,就会放到运行队列当中。
所以这种在等待特定设备的进程,叫做阻塞状态。
一个进程正在等待某一件事发生(例如请求I/O等待I/O完成等)而暂时停止运行
挂起是一个比较奇怪的概念。
假如现在有一个设备叫做磁盘,如果今天有多个进程正在等待键盘资源,这时操作系统内部的内存资源严重不足了,操作系统会在保证正常的情况下,省出来内存资源,一些在等待队列当中的资源没有被CPU调用,这些进程处于空闲状态,此时操作系统会将这些进程的PCB保留,代码和数据会放到外设当中,这个过程叫做换出,PCB在等待队列中排队,等到这个进程就绪了,在把代码和数据从外设中拿出来,放到运行队列当中,这个过程叫做换入。其中一个进程只有PCB在,代码和数据被换出了,此时这个进程的状态就是挂起状态,这样就给以给操作系统省出资源。
Linux中的进程状态
/*
* 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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
下面我们创建一个名为myproc.cpp的文件。
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "Hello World" << std::endl;
sleep(1);
}
return 0;
}
运行这个进程之后在查看进程。
发现,这个进程是S状态,myproc不是在运行吗?为什么会是S状态。
现在把代码中的输出和sleep给去了。
此时就变成了R状态。
第一次的代码带有 std::cout
看似代码一直在打印,其实是阻塞状态,当我把输出给去了,就变成了运行态(R)。
R+的意思是前台运行,在运行的时候给 ./myproc
后面加上& => ./myproc &
就变成后台运行。
#include <iostream>
#include <unistd.h>
int main()
{
std::cout << "cin>>:";
int a = 0;
std::cin >> a;
std::cout << "echo:" << a << std::endl;
return 0;
}
在运行这个程序的时候,如果没有输入,界面就会一直卡在这里,等待着输入。
查看进程
我并没有输入,所以这个进程一直在等待,直到获取到相应的资源后,才可以运行,这就是S状态。所谓的阻塞,就是在等待资源。
当获取了资源后,就是R状态了。
如果一个进程处于D状态,在Linux中也是阻塞状态,也是深度睡眠。
现在有一个进程,这个进程在向磁盘写入1G的数据,磁盘把数据接收后,存在某一区域,这个过程是要花时间的,在这期间,进程就要进行等待。完成这个操作后,磁盘向进程发起反馈,不管成功与否。那么在磁盘写入的时候,这个进程被操作系统强制干了,但是磁盘要向进程进行反馈的时候,却找不到进程了。这个数据若不重要,丢掉也无所谓;如果数据非常重要呢?丢失了就会造成大问题。所以让进程在等待磁盘写入完毕期间,这个进程不能被任何人干掉,不就行了。进程在等待磁盘写入期间,就是D状态---disk sleep
t状态被称为暂停状态。
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "Hello World" << std::endl;
sleep(1);
}
return 0; }
运行之后,进程处于S状态
通过kill -l可以查看更多的信号,之前用过kill -9 PID的命令用来杀死进程。
18和19信号分别是继续和暂停信号。
kill -19 PID
在输入 kill -18 PID
X是死亡状态。
进程终止了,就要把资源进行回收,这只是一个返回状态,不会再任务列表里看到这个状态。
Z状态也是僵尸状态。
一个进程死掉了,并不会直接进行回收,而是先将进程的退出信息维持一段时间,让关心这个进程的人知道结果和原因。维护信息的这个状态,是Z状态。
一个进程中,父进程最关心儿子进程的信息。如果父进程没有关心儿子进程,操作系统就会一直维持着儿子进程的信息。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
std::cout << "我是子进程,pid " << getpid() << "ppid" << getppid() << std::endl;
cnt--;
sleep(1);
}
exit(0);
}
else
{
while (true)
{
std::cout << "我是父进程,pid" << getpid() << "ppid" << getppid() << std::endl;
sleep(1);
}
}
return 0;
}
这个代码中,父进程并没有对子进程干任何事情,子进程结束之后,父进程就看着,啥也不干。
当运行这个程序的时候,cnt为0前,一直是都是S状态,cnt为0后,父进程是S状态,子进程变成了Z状态,既僵尸状态。
进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态。进程的相关资源尤其是task_struct 结构体不能被释放。僵尸进程会一直占用自身资源。僵尸进程不予回收就会造成内存泄漏问题。
那么我们把myproc给结束之后,为什么不会变成僵尸进程?因为bash会对myproc负责。爹只对儿子负责。
myproc结束,没有变成僵尸进程是因为bash会负责,那子进程为什么会没了?bash可不会对孙子负责。
现在不让子进程退出,让父进程退出。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 500;
while (cnt)
{
std::cout << "我是子进程,pid " << getpid() << "ppid" << getppid() << std::endl;
cnt--;
sleep(1);
}
exit(0);
}
else
{
int cnt = 5;
while (cnt)
{
std::cout << "我是父进程,pid" << getpid() << "ppid" << getppid() << std::endl;
cnt--;
sleep(1);
}
}
return 0;
}
原来子进程的父进程的PID是15604,父进程退出之后,变成了1。
如果父进程先退出,子进程的父进程会被改变成1号进程(操作系统)
父进程是1号进程---孤儿进程,该进程被系统领养。
父进程结束了,孤儿进程退出后,它的信息就没有人关心了。只能有操作系统来领养。
所以上面子进程是僵尸进程,将父进程结束后,子进程由操作系统领养,所以子进程也会结束。
僵尸进程的危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。内存泄漏
进程优先级
cpu资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可能改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
优先级是对于资源的访问,谁先访问,谁后访问。就好比买饭的时候要排队,我们已经进入了餐厅,有吃饭的权限,排队就是决定谁先吃,谁后吃的问题。
为什么会有优先级呢?
学生在学校要吃饭,如果给每个学生配一个厨师,那就不需要在餐厅排队吃饭了。但学生这么多,不可能实现每个学生配一个厨师。一个系统中有好多进程,这些进程不可能都配一个CPU,且资源是有限的,所以这些进程就需要对CPU进行竞争。操作系统必须保证大家良性竞争,就要确认优先级。如果不保证良性竞争,那结果就是谁nb谁资源多,这样会造成一些进程长时间得不到CPU资源,该进程的代码长时间无法得到推进---该进程的饥饿问题。
当然不要去修改优先级,调度器会自己解决优先级的问题。
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "This is a process." << std::endl;
sleep(1);
}
return 0;
}
运行之后查看进程 ps -al
PRI就是优先级(priority)
NI就是nice值:进程优先级的修正数据。
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
Linux不会让你随便的调整优先级,所以nice值给出的有限制。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据
如何更改优先级呢?
top命令。
然后输入r
在输入PID,
在输入新的nice值
就可以完成修改了。
如果新的nice值不在ni的区间,系统会自动的找在区间内的最大值。
那么操作系统是如何根据优先级开展的调度呢?
通过位图。可以近乎O(1)的时间复杂度来调度。
其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性 。
我们写的代码,在编译后要运行的时候,需要带 ./
才能运行。而指令,则不需要带 ./
就可以运行?指令和我们写的代码,都在目录中,这些目录还有什么区别吗?
系统当中,针对于指令的搜索,Linux会提供一个环境变量PATH。
通过 echo $PATH
可以打印环境变量
这些路径以冒号为分隔符,在执行指令的时候,系统会在上面的路径中,寻找指令。所以我们自己写的代码,只写文件名,系统在路径中搜索不到,也就无法运行了。
那么,将这个proc的路径,添加到PATH中,是不是就可以输入名字运行代码了呢?
这样的写法是覆盖PATH,会将之前的路径都覆盖。在使用了之后绝大部分指令就不能运行了。因为系统找不到对应的路径。就算覆盖了,不用担心,PAHT会在启动的时候加载到内存,重启一下,就会恢复。
将路径添加到PATH后,就可以输名字,运行代码了。
输入 env可以查看环境变量。
我们在终端输入指令的时候,会自动的记录上一条输入的指令,这个记录的指令是有限制的,HISTSIZE就是能够记录的条数
USER代表的用户。
PWD代表当前路径
通过getenv函数可以获取环境变量。
#include <iostream>
#include <string>
#include <stdlib.h>
int main()
{
std::string who = getenv("USER");
if (who == "root")
{
std::cout << "允许做任何事情" << std::endl;
}
else
{
std::cout << "你就一个普通用户,没有足够权限" << std::endl; }
return 0;
}
环境变量是系统提供的一组name=value形式的变量,不同的环境变量有不同的用户,通常具有全局属性。
命令行参数
在学C语言的时候,一些教材上main函数是带参数的。
int main(int argc, char *argv[])
{
}
C/C++的main函数,是可以传参的,上面两个,就是参数。
#include <stdlib.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
for (int i = 0; i < argc; i++)
{
printf("argv[%d]->%s",i,argv[i]);
}
return 0;
}
当我们运行代码的时候, 你以为你输入的是 ./proc -a -b -c
,开始做命令行解释的时候,会把这个字符串打散成四个字符串,再将每个字符串的起始地址,保存在argv中,然后传递给main函数,argc中存数量,argv中存的字符串。
为什么要这么做呢?
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage: %s -[a|b|c|d]\n",argv[0]);
}
if (strcmp(argv[1],"-a") == 0)
{
printf("aaa\n");
}
else if (strcmp(argv[1],"-b") == 0)
{
printf("bbb\n");
}
else if (strcmp(argv[1],"-c") == 0)
{
printf("ccc\n");
}
else if (strcmp(argv[1],"-d") == 0)
{
printf("ddd\n");
}
else
{
printf("defaul\n");
}
return 0;
}
同一个指令,不同的选项,可以得到不同的结果。
这样可以为指令,工具,软件等提供命令行选项的支持。比如 ls -a,ps ajx
等。
还可以带第三个参数---环境变量列表。
int main(int argc,char *argv[],char *env[])
{
}
我们所运行的进程,都是子进程,bash本身在启动的时候,会从操作系统的配置文件中读取环境变量信息,子进程会继承父进程交给我的环境变量。
怎么证明这件事?
在命令行上这样搞,并不是环境变量,但确实存在。
这是本地变量。
所谓的本地变量就是在命令行中定义变量。本地变量不会被继承
a = 1
b = 2
c = 3
echo $a可以输出a。但如果敲一个代码看这个变量是无法找到的,echo却可以输出。
两批命令:
常规命令:通过创建子进程完成的。
内建命令:bash不创建子进程,而是由自己亲自执行,类似于bash调用了自己写的,或者系统提供的函数。
set命令可以查看系统当中的所有变量。
现在将这个变量定义为环境变量。
只需要带个export将它导出即可。
在运行代码后,发现,会继承。
unsert MY_VALUE
会取消环境变量。