什么是程序?
程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁…)
什么是进程?
进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)
站在程序员的角度:运行一系列指令的过程。
站在操作系统角度:分配内存单元的基本单位。
区别:
程序占用磁盘,不占用系统资源。
进程占用系统资源。
一个程序对应多个进程,一个进程对应一个程序。
程序没有生命周期,进程有生命周期。
时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。
1.虚拟内存到内存的映射
2.修改内存访问级别
用户空间映射的物理内存是独立的。
每个进程在内核中都有一个进程控制块来维护进程相关的信息,linux内核的进程控制块是task_struct结构体。
通过这个命令进行查找:sudo grep -rn “struct task_struct {” /usr/
光标停留在{上,按%,可以到结构体的结尾,400多行。
内部成员定义:
1.进程id.系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
2.进程的状态,有就绪,运行,挂起,停止等状态。
3.进程切换时需要保存和恢复的一些CPU寄存器。(主要指CPU寄存器值)
4.描述虚拟地址空间的信息(虚拟地址和物理地址之间的映射关系,由MMU进行维护)。
5.描述控制终端的信息
6.当前工作目录
7.umask掩码
8.文件描述符表,包含很多指向file结构体的指针。
9.和信号相关的信息
10.用户id和组id
11.会话和进程组
12.进程可以使用的资源上限
环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:
① 字符串(本质) ② 有统一的格式:名=值[:值] ③ 值用来描述进程环境信息。
存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量。extern char ** environ;
常见环境变量
按照惯例,环境变量字符串都是name=value这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH
可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:
$ echo $PATH
SHELL
当前Shell,它的值通常是/bin/bash。
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
LANG
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
#include
#include
extern char **environ;
int main(void)
{
int i;
for (i = 0; environ[i] != NULL; i++)
printf("%s\n", environ[i]);
return 0;
}
getenv函数
获取环境变量值
char *getenv(const char *name); 成功:返回环境变量的值;失败:NULL (name不存在)
练习:编程实现getenv函数。 【getenv.c】
setenv函数
设置环境变量的值
int setenv(const char *name, const char *value, int overwrite); 成功:0;失败:-1
参数overwrite取值: 1:覆盖原环境变量
0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)
unsetenv函数
删除环境变量name的定义
int unsetenv(const char *name); 成功:0;失败:-1
注意事项:name不存在仍返回0(成功),当name命名为"ABC="时则会出错。
fork函数
创建一个子进程:
pid_t fork(void);失败返回-1;成功返回:(1)父进程返回子进程的ID(非负)(2)子进程返回0
pid_t类型表示进程ID,但为了表示-1,它是有符号整型,(0不是有效进程ID,init最小,为1)注意返回值,不是fork函数返回能返回两个值,而是fork后,fork函数变为两个,父子需各自返回一个。
获得pid,进程id,获得当前进程
pid_t getpid(void);
获得当前进程父进程的id
pid_t getppid(void);
#include
#include
#include
int main(void)
{
int i;
pid_t pid;
printf("xxxxxxxxxxx\n");
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (i < 5) {
sleep(i);
printf("I'am %d child , pid = %u\n", i+1, getpid());
} else {
sleep(i);
printf("I'm parent\n");
}
return 0;
}
父子进程之间在fork后,有哪些相同,哪些不同之处呢?
刚fork之后:
父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式。
父子不同处:1.进程ID 、2.fork返回值、 3.父进程ID 4.进程运行时间、5.闹钟(定时器)6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但PID不同。真的是每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存空间。
#include
#include
#include
int var = 34;
int main(void)
{
pid_t pid;
pid = fork();
if (pid == -1 ) {
perror("fork");
exit(1);
} else if (pid > 0) {
sleep(2);
var = 55;
printf("I'm parent pid = %d, parentID = %d, var = %d\n", getpid(), getppid(), var);
} else if (pid == 0) {
var = 100;
printf("child pid = %d, parentID=%d, var = %d\n", getpid(), getppid(), var);
}
printf("var = %d\n", var);
return 0;
}
【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
作用是执行其他程序:
其实有六种以exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
execlp函数
加载一个进程,借助PATH环境变量
int execlp(const char *file, const char *arg, …); 成功:无返回;失败:-1
参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
#include
#include
#include
int main(void){
pid_t pid;
pid=fork();
if(pid==-1){
perror("fork error:");
exit(1);
}
else if(pid>0){
sleep(1);
printf("parent\n");
}
else{
execlp("ls","ls","-l","-a",NULL);
}
return 0;
}
execl函数
加载一个进程, 通过 路径+程序名 来加载。
int execl(const char *path, const char *arg, …); 成功:无返回;失败:-1
对比execlp,如加载"ls"命令带有-l,-F参数
execlp(“ls”, “ls”, “-l”, “-F”, NULL); 使用程序名在PATH中搜索。
execl("/bin/ls", “ls”, “-l”, “-F”, NULL); 使用参数1给出的绝对路径搜索。
一次wait或waitpid调用只能清理一个进程,清理多个子进程应使用循环。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:即子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程。
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。 特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
#include
#include
#include
#include
int main(void)
{
pid_t pid, wpid;
pid = fork();
if (pid == 0) {
printf("---child, my parent= %d, going to sleep 10s\n", getppid());
sleep(10);
printf("-------------child die--------------\n");
} else if (pid > 0) {
wpid=wait(NULL);
if(wpid==-1){
perror("wait error:");
exit(1);
}
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出
② 回收子进程残留资源
③ 获取子进程结束状态(退出原因)。
pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
#include
#include
#include
#include
int main(void)
{
pid_t pid, wpid;
pid = fork();
int status;
if (pid == 0) {
printf("---child, my parent= %d, going to sleep 10s\n", getppid());
sleep(20);
printf("-------------child die--------------\n");
exit(77);
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
wpid = wait(&status);
if (wpid == -1) {
perror("wait error");
exit(1);
}
if (WIFEXITED(status)) {
//为真说明子进程正常结束
printf("child exit with %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
//为真说明子进程被信号终止(异常)
printf("child is killed by %d\n", WTERMSIG(status));
}
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
一个wait调用只回收一个子进程。
getgid函数
获取当前进程使用用户组ID
gid_t getgid(void);
获取当前进程有效用户组ID
gid_t getegid(void);
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, int options); 成功:返回清理掉的子进程ID;失败:-1(无子进程)
特殊参数和返回情况:
参数pid:
0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程
返回0:参3为WNOHANG,且子进程正在运行。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
作业:父进程fork 3 个子进程,三个子进程一个调用ps命令, 一个调用自定义程序1(正常),一个调用自定义程序2(会出段错误)。父进程使用waitpid对其子进程进行回收。
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int n = 5, i; //默认创建5个子进程
pid_t p, q;
pid_t wpid;
if(argc == 2){
n = atoi(argv[1]);
}
for(i = 0; i < n; i++) {
//出口1,父进程专用出口
p = fork();
if(p == 0) {
break; //出口2,子进程出口,i不自增
} else if (i == 3){
q = p;
}
}
if(n == i){
sleep(n);
printf("I am parent, pid = %d\n", getpid(), getgid());
waitpid(q,NULL,0);//只回收了一个子进程
while(waitpid(-1,NULL,0));//==wait(NULL);
do{
wpid=waitpid(-1,NULL,WNOHANG){
if(wpid>0){
n--;
}
//if wpid==0说明子进程正在运行
sleep(1);
}
}
//pid_t pid = waitpid(q, NULL, WNOHANG);
// pid_t pid = wait(NULL);
//printf("child pid = %d\n", pid);
while(1);
} else {
sleep(i);
printf("I'm %dth child, pid = %d, gpid=%d\n",
i+1, getpid(), getgid());
while(1);
}
return 0;
}