一,进程
1. 进程的概念
简单的说,一个执行中的程序就是一个进程。具体说, 进程是一个分配系统资源的活动实体,它包含以下两点:
(1)实体,每个进程都有一段地址空间,在这段空间中,用于存放程序的代码,数据,指令的地址等静态的事物;
(2)程序运行的过程,是一个动态的概念,即需不断申请占用系统资源的当前的活动过程。
总之,进程不仅包含有关程序的一些静态事物,还有需不断申请系统资源的动态的过程。
2. 进程的创建过程
进程形成的方法:
(1)一个程序要运行起来,首先要被加载到内存上,这时,就形成了进程。该进程的父进程是bush。
(2)通过系统调用来创建,在一个进程中利用fork,vfork函数创建子进程,该子进程的父进程就是创建它的的进程。(关于fork,vfork的使用,在后文中有详细说明)
形成之后:
操作系统要对该进程进行管理,但并不和该进程的程序代码等直接接触。而是:
(1)描述:将该进程的相关属性信息描述起来,放在一个叫进程控制块的数据结构中,将进程控制块统称为PCB,在Linux中称PCB为task_struct,task_struct由操作系统创建。
(2)组织:操作系统将各进程的PCB利用链表的形式组织起来,放在内核中。这样,操作系统就可以对各进程进行统一管理了。
(3)为各进程分别创建一个虚拟地址空间(也是一个结构体:mm_struct),页表,映射关系。
各进程的相关代码和数据都存放在物理内存中,但操作系统并不直接对物理内存进行操作,而是将虚拟地址通过页表映射到物理内存地址上,从而对其进行操作。该虚拟地址空间由上述的PCB中的指针指向,这样,操作系统便可管理该进程了。
总之,一个进程的创建,操作系统需:
(1)描述并创建PCB,并将其链入链表组织起来放入内核
(2)创建虚拟地址空间,页表,映射关系
3. task_struct的内容
标识符PID:描述本进程的唯一标识符,用于区别其他进程,类似于学生的学号,人的身份证号。
状态:包括任务状态,退出代码,退出信号等。
优先级:一个CPU一次只能运行一个进程,运行哪个进程要根据其优先级来判断
程序计数器:存放程序中即将被执行的下一条指令的地址,类似于寄存器PC的存在
内存指针:包含程序代码和进程数据相关的指针,还有和其他进程共享的内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备,被进程使用的文件列表
记账信息:包括进程存在的时间总和等。
其中:
进程的状态包含:
(1)R:运行状态,处于该状态的进程要么在运行要么处于运行队列中
(2)S:浅度睡眠状态,可以被中断,可以用kill命令杀死
(3)D:深度睡眠状态,不可被中断,不可被杀死,一般在I/O时发生,要结束该状态的进程,要么等I/O结束,要么关机
(4)T:停止状态
(5)Z:僵尸状态:进程退出后一直维持该状态,直到该进程的父进程读取了他的退出信息后,将其释放。
状态的优先级:CPU根据优先级来决定运行进程的先后顺序
可以通过ps -l命令来查看优先级,其中:
PRI:表示进程的优先级,此值越小优先级越高
NI:优先级的修正数值
所以,PRI=PRI(80)+NI(-20~19)
可以通过命令来修改NI:
(1)renice:修改已存在进程的优先级:renice NI的值 -p 要修改进程PID
(2)nice:修改启动前的进程的优先级:nice -n NI的值 可执行程序路径
运行如下进程:
#include<stdio.h>
int main()
{
while(1)
{
sleep(1);
}
return 0;
}
执行如下命令:
或者,在进程启动前,输入以下命令:
4. 查看进程
在一个进程运行过程中,需要知道如何查看该进程是否被创建成功。
(1)Linux下输入命令:
ls /proc
该命令用于显示进程目录/proc中所有当前正在运行的进程
这些数字即为正在运行进程的PID。
(2)通过命令ps或top获取进程信息。
(3)利用系统调用获取进程标识符
pid_t getpid();//获取当前进程的PID
pid_t getppid();//获取当前进程的父进程的PID
这两个函数均需引用头文件<sys/types.h>和<unistd.h>
5. 创建进程
在了解了进程的相关信息后,接下来,我们来创建一个进程。
(1)通过运行程序来创建
注意:进程是一个动态的过程,程序在运行期间,进程被创建,一旦程序运行结束,进程也就退出了。
那么,程序在一瞬间就运行结束,我们如何知道进程是否被创建呢?
编写如下代码,使程序一直运行:
#include<stdio.h>
int main()
{
while(1)
{
sleep(1);
}
return 0;
}
编译链接运行该程序:
我们发现该程序一直在运行,也就是该进程一直存在,但怎么获取该进程相关的信息呢?
这时,需要该进程在后台运行,从而获取其相关信息,输入以下指令:
输出的数字为该进程的PID,这时,查看/proc目录,可以看到该进程的PID在该目录中。说明该进程已经被创建成功,当输入:
kill -9 7856(进程的PID)
可通过该命令杀死该进程,此时,在查看/proc目录时,已经没有该进程的PID了。说明该进程已经结束。
(2)利用fork函数创建子进程
fork创建子进程后,子,父进程对fork语句之后的代码和没有修改的数据进行共享,如果数据被父或子进程修改,则在父,子进程中会各自开辟存放该数据的内存,采取写实拷贝。
fork函数的返回值情况:
1)若子进程创建成功,向父进程返回子进程的PID,向子进程返回0;
2)若子进程创建失败,则返回-1。
运行如下程序:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int ret=fork();
if(ret<0)
{
printf("fork error!\n");
return 1;
}
else if(ret == 0)//子进程
{
printf("child:pid=%d,ppid=%d,ret=%d\n",getpid(),getppid(),ret);
}
else//父进程
{
printf("farther:pid=%d,ppid=%d,ret=%d\n",getpid(),getppid(),ret);
}
sleep(1);
return 0;
}
运行结果:
可以看到,在子进程创建后,先运行父进程,在运行子进程,返回值情况确实满足上述两点。
注意:fork创建子进程后,谁先运行是不确定的。
(3)利用vfork创建子进程
vfork的用法与fork相同,只是,有下列区别:
1)vfork创建子进程后,子,父进程共享代码和数据
2)先运行子进程,子进程只有通过调用exit或exec结束后,父进程才开始运行
6. 僵尸进程
就是处于僵尸状态的进程,当子进程退出,父进程没有读取子进程的退出信息时,子进程就一直处于僵尸状态。
运行以下程序来创建僵尸进程:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int ret=fork();
if(ret<0)
{
printf("fork error!\n");
return 1;
}
else if(ret>0)//父进程
{
printf("farther:pid=%d,ppid=%d,ret=%d\n",getpid(),getppid(),ret);
sleep(30);//30s后父进程退出
}
else//子进程
{
printf("child:pid=%d,ppid=%d,ret=%d\n",getpid(),getppid(),ret);
sleep(5);//5s后子进程退出
exit(1);
}
return 0;
}
在该程序运行5s内,子,父进程都在运行。5s后,子进程运行结束,此时,父进程还在运行,还没来得及获取子进程的退出信息,所以,5s后子进程变为僵尸进程;30s后父进程运行结束,子父进程均退出。
输入以下命令,观察处于僵尸状态的子进程:
进程开始运行后5s内:
5s后:
30s后:
7. 孤儿进程
如果父进程先退出,而子进程还在进行,此时,子进程就会变成孤儿进程。 孤儿进程会被1号进程领养,该孤儿进程退出后,就会被1还进程回收。
下面,演示一个孤儿进程的例子:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int ret = fork();
if(ret == -1)
{
perror("fork error\n");
return 1;
}
else if(ret > 0)//父进程,先结束
{
printf("pid = %d,ppid = %d ret = %d\n",getpid(),getppid(),ret);
}
else//子进程,30s后再结束
{
printf("pid = %d,ppid = %d ret = %d\n",getpid(),getppid(),ret);
sleep(30);
}
return 0;
}
在该例中,父进程执行完毕后,即退出,而子进程30s后才退出,所以,来观察下子进程的父进程的PID来验证它是否被1号进程领养。
结果如下:
二,环境变量
环境变量是操作系统中指定程序运行环境的一些变量,比如,在链接库文件时,这些库文件在哪里,去哪里找,这些信息都存放在环境变量中。
环境变量通常具有某些特殊用途,在系统中通常具有全局特性。
1. 常见环境变量
(1)PATH:存放系统常用命令的搜索路径的变量
查看该变量的内容,输入命令:
echo $PATH
输出结果:
/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/admin/bin
每个路径用分号相隔。比如,常用的命令ls,我们在运行它的时候,并没有加上的路径,并不知道该命令的可执行程序放在哪里,这时,操作系统,就会根据PATH中提供的路径,一条路径一条路径的找,直到找到为止。
我们自己编写形成的可执行程序a.out在运行时,必须加上路径,如:./a.out,表示运行当前路径下的a.out。那如何能像运行ls一样不加路径呢,有如下两种方法:
1)将可执行程序a.out放在PATH指定的路径下,那么在运行时,就会根据PATH中的路径去找a.out所在的位置。
如:cp ./a.out /bin
2)将a.out的路径放入到PATH变量中。这时,需要先明白a.out的路径,再进行存放,输入命令:
$PATH表示原有的内容,即上述查看PATH时输出的内容。 :/home/admin/bit_code/Linux表示在原有PATH的基础上追加该路径;然后把原有PATH的内容和追加后的路径一起赋给变量PATH;export是导出变量PATH,即更新PATH。
再次进行查看时,便可看到a.out的路径已经在变量PATH中:
若要删除新存放的路径,只需要关闭终端,再次打开即可。
如果不想关闭终端来删除新追加的路径,只需输入以下命令即可:
export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/admin/bin
(2)HOME:指定用户的主工作目录(可以用echo命令来进行查看)
(3)HISTSIZE:系统中默认保存的历史命令的条数,一般是1000。(以同样的方法可查看)
若要查看历史命令,只需输入命令:history
(4)SHELL:查看当前使用的shell
2. 常用的有关环境变量的命令
(1)echo:查看环境变量的内容,上述已经说明
(2)export:设置一个新的环境变量
1)更新原有环境变量的值,(这里不加export也可以,因为该变量已经是环境变量,但最好加上)上述已经验证
2)将本地变量变成环境变量。
将MYENV设置为环境变量,在终端运行以下命令:
MYENY="hello world"
用echo查看时,没有结果,说明该环境变量没有设置成功,他只是一个本地变量。
再在终端运行以下命令:
export MYENY="hello world"
用echo查看时,可以看到结果,说明该环境变量设置成功。
所以,export的作用是设置环境变量。如果环境变量已经存在,可以理解为更新环境变量的值(也可以不加);如果环境变量不存在,就必须用export设置环境变量。
注意:使用export设置的环境变量在关闭中断后即失效
(3)env:查看系统中存在的所有环境变量及其值
(4)set:显示所有本地定义的shell变量及环境变量和其相应的值(注意与env的区别)
运行以下命令:
MYENV="hello world"
上述已经说明,该变量不是环境变量(用env查看没有结果),所以是本地变量,用set查看:
set | grep MYENV
这时会显示结果:
MYENV='hello world'
说明,set可以显示本地变量,同样可以显示环境变量,这里不再说明。
(5)unset:清除环境变量
先用export设置环境变量MYENV,再用env查看是否创建成功,创建成功后,在用unset清除该环境变量,在用env查看是否清除成功。如下:
3. 环境变量的全局特性
首先通过命令在终端设置本地变量:MYENV=“hello world”,然后通过以下程序(即子进程,bash为父进程)输出该环境变量的值。
下面先介绍一个系统调用getenv
#include <stdlib.h>//需引用的头文件
char *getenv(const char *name);//函数原型,该函数用于获取指定环境变量的值,参数是环境变量的名称字符串
再运行以下代码,输出环境变量的值:
#include<stdio.h>
#include<stdlib.h>
int main()
{
char *env=getenv("MYENV");
if(env)
{
printf("%s\n",env);
}
return 0;
}
当运行该代码时,出现如下结果:
发现没有结果,说明子进程不能看到本地变量的值。
通过export将MYENV设置为环境变量,在运行程序时,会输出:hello world。说明,此时子进程能够看到环境变量的值。因为环境变量具有全局特性。
注意:本地变量没有全局特性,只会被本shell看到,不会被子进程看到。
而环境变量具有全局特性,所以会被本shell及以下的所有子进程看到,即被子进程继承下去。