文章目录
- Linux进程
- 1、计算机体系结构和操作系统管理
- 1.1、计算机体系结构 -- 硬件
- 1.2、操作系统(Operator System) -- 软件
- 2、进程
- 2.1、进程基本概念
- 2.2、进程标识符
- 2.2.1、获取当前进程标识符和当前进程的父进程标识符
- 2.2.2、通过系统调用创建进程 -- fork初识
- 2.2.3、更改当前工作目录
- 2.3、进程状态
- 2.3.1、进程任务状态
- 2.3.2、进程等待
- 2.3.3、进程终止
- 2.3.3.1、进程退出代码
- 2.3.3.2.、进程退出信号
- 2.4、进程优先级
- 2.5、进程程序替换
- 2.6、环境变量
- 2.6、进程地址空间
- 2.7、Linux2.6内核进程调度队列
- 3、自制Shell
Linux进程
1、计算机体系结构和操作系统管理
1.1、计算机体系结构 – 硬件
-
目前我们常用的计算机,都是采用冯·诺依曼体系结构。
-
冯·诺依曼提出了电子计算机系统制造的三个基本原则,即采用二进制逻辑、程序存储执行以及电子计算机系统由五个部分组成(运算器、控制器、存储器、输入设备、输出设备,其中运算器与控制器又共同组成为中央处理器CPU),这套理论被称为冯·诺依曼体系结构。
-
其中:
-
输入单元:包括键盘, 鼠标,扫描仪, 写板等
-
中央处理器(CPU):含有运算器和控制器等
-
输出单元:显示器,打印机等
-
存储器:这里一般都指内存
-
-
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。
-
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
-
总结:所有设备都只能直接和内存打交道。
-
-
那么为什么CPU不能直接和外设直接打交道?
原因是CPU速度很快,外设速度很慢,如果CPU与外设直接进行数据传输会导致CPU的大部分时间在等待外设的输入,非常浪费CPU的速度。那么如果在CPU和外设之间加上一个内存,CPU与内存的数据传输会快很多,外设的一些数据可以在CPU还没有运行到这部分的时候先导入到内存中等待,CPU运行时候,直接从内存中拿就行,进而提高传输效率。
-
我们来看各存储单元的运行速率,是遵循一个金字塔的模式。
我们可以看到,存储单元离CPU越近,速度更快,但是造价更贵,单体容量越小;存储单元离CPU越远,速度更慢,但是造价更便宜,单体容量越大。
-
1.2、操作系统(Operator System) – 软件
-
操作系统是对计算机软硬件资源管理的软件。
-
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
-
内核(进程管理,内存管理,文件管理,驱动管理)
-
其他程序(例如函数库,shell程序等等)
-
-
操作系统管理:先描述被管理对象,再组织被管理对象。
-
底层硬件:其实就是冯·诺依曼体系结构。
-
驱动程序:驱动程序是计算机系统中的软件,作为硬件与操作系统之间的桥梁,实现操作系统对硬件设备的控制和数据交互,确保系统正常运行。
-
操作系统:对计算机软硬件资源管理的软件。
-
系统调用接口:操作系统提供给用户的接口,比如用户需要执行一些特权操作,则需要进行系统调用才能访问到内核数据(
trap
、getpid()
等)。(比如会接触到硬件的函数调用) -
用户操作接口:设计者提供的。比如
lib
即库,针对不同的操作系统(如Linux和windows),在C语言编译器中都能用printf
函数,说明库函数里面不止实现了某一个操作系统的printf
函数。 -
用户:经过上述管理,用户只需要进行可视化操作。
-
-
操作系统对各个模块(内存、进程、文件、驱动等)的管理都是采用链表进行管理(其实就是对PCB(进程控制块 – 后面涉及)、FCB(文件控制块)等进行管理)。
比如对设备进行管理,那么先定义一个设备的结构体
struct device{ char name[]; char status[]; ... struct device* next; }
接下来如果有多个设备需要运行,则使用链表进行排队。
这就是所谓的先描述,再组织的管理思想。
-
为什么要有对操作系统的管理
- 对下管理好软硬件资源 – 手段
- 对上提供一个良好(稳定、高效、安全)的运行环境 – 目的
- 如果没有操作系统管理软硬件资源,即如果用户直接管理软硬件资源,用户可能会有不安全的操作(用户1可以拿用户2的数据),导致严重的后果。
2、进程
我们在前面有提到操作系统对外设的管理是先描述,再组织。那么操作系统对进程的管理是怎么样的?同样,先描述,再组织。
2.1、进程基本概念
-
进程是计算机系统中的执行实体,是程序在执行过程中分配和管理资源的基本单位。每个进程都有独立的内存空间、代码、数据和系统资源,进程之间通过通信机制进行交互。进程的创建、调度和终止由操作系统管理,使得多个任务能够并发执行,提高系统的效率和资源利用率。
-
进程 = PCB(进程控制块) + 程序。
-
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block)。
//举例 struct PCB{ //状态 //优先级 //内存指针 //标识符 //...包含进程几乎所有的属性字段 struct PCB* next; }
-
Linux操作系统下的PCB是: task_struct
-
task_ struct内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
-
查看进程:
ls \proc
查看进程,ls \proc\1
可以查看进程id
为1
的进程信息,说明进程在计算机的存储方式也是一个文件夹。-
大多数进程信息同样可以使用
top
和ps
这些用户级工具来获取。top
:ps axj | head -1 && ps axj | grep process | grep -v grep
:#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { while(1){ sleep(1); } return 0; }
-
2.2、进程标识符
进程id(PID)
父进程id(PPID)
2.2.1、获取当前进程标识符和当前进程的父进程标识符
-
使用
getpid()
函数,这个函数是使用了系统调用接口。使用指令man getpid
查看使用的库及返回值。#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main(){ printf("当前进程id:%d,当前进程的父进程id:%d\n",getpid(),getppid()); return 0; }
2.2.2、通过系统调用创建进程 – fork初识
通过
man fork
查看fork
用法
fork()
函数有两个返回值,子进程返回0
,父进程返回子进程id,创建不成功返回-1
父子进程代码
fork
之后共享,数据各自开辟空间,私有一份(采用写时拷贝)。写时拷贝:在Linux中,写时拷贝(Copy-On-Write,简称COW)是一种用于提高系统性能和节省资源的策略。它通常应用于进程创建和文件复制等场景。以下是一些相关的简要介绍:
进程创建:
- 当一个进程通过fork()系统调用创建子进程时,子进程并不立即复制父进程的整个地址空间。
- 相反,子进程会共享父进程的地址空间,只有在其中一个进程试图修改共享的内存页时,才会进行实际的复制(写时拷贝)。
文件复制:
- 当使用类似于cp命令进行文件复制时,操作系统也可以利用写时拷贝的策略。
- 初始阶段,新文件与原文件共享相同的数据块。只有在其中一个文件尝试修改数据时,才会复制数据块,确保每个文件有自己的副本。
写时拷贝的优势在于节省内存和提高效率。由于父子进程或复制的文件共享相同的资源,不需要立即进行复制,从而减少了内存的使用和提高了系统的性能。
-
测试fork是怎么有两个返回值的。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { int ret = fork(); printf("hello proc : %d!, ret: %d\n", getpid(), ret); sleep(1); return 0; }
- 这里我们看到,
printf
打印了两次,根据我们查询的fork
的用法,知道pid
为1653
的为父进程,pid
为1564
的是子进程!这里还有一个问题,父子进程用的是同一个ret
,为什么返回值是不同的?(后面涉及到虚拟地址空间再详述)
- 这里我们看到,
-
通常,
fork
之后要用if
分流。#include <stdio.h> #include <sys/wait.h> #include <sys/types.h> #include <unistd.h> int main() { int ret = fork(); if(0 == ret){ int cnt = 5; while(cnt){ printf("I am child process : id: %d!, ret: %d\n", getpid(), ret); sleep(1); cnt--; } exit(0); } //只有父进程才能运行到这里 pid_t id = wait(NULL); int cnt = 10; while(cnt){ printf("I am father process : id: %d!, ret: %d\n", getpid(),ret); sleep(1); cnt--; } printf("等待子进程成功,子进程ID: %d\n",id); return 0; }
2.2.3、更改当前工作目录
-
使用
chdir
#include <stdio.h> #include <unistd.h> int main(){ printf("I am a process,pid: %d ,ppif: %d\n",getpid(),getppid()); printf("更改工作目录前\n"); sleep(15); chdir("/home/xp2/test_change_workdir"); printf("更改工作目录后\n"); FILE* pf = fopen("./110.txt","w"); sleep(5); printf("创建文件成功\n"); fclose(pf); pf = NULL; return 0; }
2.3、进程状态
2.3.1、进程任务状态
进程状态查看:ps axj | head -1 && ps axj | grep 进程名称 | grep -v grep
,如果想要每隔1秒查看一次:while :;do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; done
。
看看内核代码怎么定义进程状态的:
/* * 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): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行(就绪)队列里。进程不是一直在运行的,一般都是采用时间片策略(每隔一段时间,这个时间非常短,CPU轮换的运行不同的进程),没有在运行的进程,如果只差CPU一种资源的话,就是就绪状态(这里我们统称为运行状态R)。
#include <stdio.h> int main(){ printf("准备开始while循环\n"); sleep(3); while(1){ // printf("查看进程现在状态\n"); } return 0; }
S-睡眠状态(sleeping): 意味着进程在等待事件完成(阻塞)(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
#include <stdio.h> int main(){ int input = 0; scanf("%d",&input); printf("input = %d\n",input); sleep(3); return 0; }
D-磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T-停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X-死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z(zombie)-僵尸进程:
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时(读取使用wait()系统调用,后面讲)就会产生僵死(尸)进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。并且如果父进程一直不读取子进程状态,子进程会一直保持Z状态,直到父进程退出(如果父进程比子进程先退出,则子进程的状态为S(子进程变为孤儿进程(后面会说到),父进程
id
变为1),直到进程退出)。#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(){ printf("马上准备创建子进程\n"); pid_t id = fork(); if(id < 0) perror("fork error\n"); else if(id == 0) { //子进程 int cnt = 5; while(cnt){ printf("I am child ,pid: %d ,ppid: %d\n",getpid(),getppid()); sleep(1); cnt--; } } else{ //父进程 int cnt = 10; while(cnt){ printf("I am father ,pid: %d ,ppid: %d\n",getpid(),getppid()); sleep(1); cnt--; } } return 0; }
僵尸进程的危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
内存泄漏?是的!
如何避免?wait()或者waitid()(后面讲)。
wait()函数
#include <stdio.h> #include <sys/wait.h> #include <sys/types.h> #include <unistd.h> int main() { int ret = fork(); if(0 == ret){ int cnt = 5; while(cnt){ printf("I am child process : id: %d!, ret: %d\n", getpid(), ret); sleep(1); cnt--; } exit(0); } //只有父进程才能运行到这里 pid_t id = wait(NULL); int cnt = 10; while(cnt){ printf("I am father process : id: %d!, ret: %d\n", getpid(),ret); sleep(1); cnt--; } printf("等待子进程成功,子进程ID: %d\n",id); return 0; }
孤儿进程:
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”,子进程的
ppid
变为1。孤儿进程被1号
init
进程领养,当然要有init
进程回收喽。#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(){ printf("马上准备创建子进程\n"); pid_t id = fork(); if(id < 0) perror("fork error\n"); else if(id == 0) { //子进程 int cnt = 10; while(cnt){ printf("I am child ,pid: %d ,ppid: %d\n",getpid(),getppid()); sleep(1); cnt--; } } else{ //父进程 int cnt = 5; while(cnt){ printf("I am father ,pid: %d ,ppid: %d\n",getpid(),getppid()); sleep(1); cnt--; } } return 0; }
2.3.2、进程等待
进程等待必要性:
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的方法:
wait
方法:#include<sys/types.h> #include<sys/wait.h> pid_t wait(int *status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid
方法:pid_t waitpid(pid_t pid, int *status, int options); 返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数: pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。 status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG(加入这个参数,那么进程就是非阻塞等待。不加入这个参数,及参数为0,则进程是阻塞等待): 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
获取进程的
status
:
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
举例:
process_wait.c
文件#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #define NUM 5 typedef void(*fun_t)(); fun_t tasks[NUM]; //任务/ void printLog(){ printf("this is a printLog\n"); } void flushNPC(){ printf("this is a flushNPC\n"); } void printNet(){ printf("this is a printNet\n"); } void initTasks(){ tasks[0] = printLog; tasks[1] = flushNPC; tasks[2] = printNet; tasks[3] = 0; } void executeTasks(){ for(int i=0; tasks[i]; ++i){ tasks[i](); } } int main(){ initTasks(); printf("I am a process! pid=%d ,ppid=%d\n",getpid(),getppid()); pid_t id = fork(); if(id < 0) perror("fork\n"); else if (id == 0){ //子进程 int cnt = 10; while(cnt){ printf("I am child process! pid=%d ,ppid=%d\n",getpid(),getppid()); sleep(1); --cnt; } exit(111); } //父进程 int status = 0; while(1){ pid_t rid = waitpid(id,&status,WNOHANG);//非阻塞等待 if( 0 == rid ){ //子进程没有结束,我们父进程可以做一些其他事 printf("during wait child ,do other things ....\n"); printf("############ tasks begin ###########\n"); executeTasks(); printf("############ tasks end ###########\n"); } else if (rid > 0){ //进程正常终止 printf("wait child success,status:%d,exitcode:%d\n",status,WEXITSTATUS(status)); break; } else{ perror("waitpid error\n"); break; } sleep(1); } //pid_t rid = waitpid(id,&status,0);//阻塞等待 // if(WIFEXITED(status)){ // //进程正常终止 // printf("wait child success,status:%d,exitcode:%d\n",status,WEXITSTATUS(status)); // }else{ // printf("waitpid error,exitsignal:%d\n",status&0x7F); // exit(-1); // } //} return 0; }
运行结果:
2.3.3、进程终止
- 进程退出场景:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
- 进程退出方法:可以通过
echo $?
查看进程退出码。
- 正常终止:
- 从main返回
- 调用exit
- 调用_exit
- 异常终止:
- ctrl + c,信号终止
2.3.3.1、进程退出代码
在C语言中,
printf
函数通常需要遇到换行符\n
或者程序正常结束(return 0)时,才会将缓冲区中的内容刷新到标准输出。这是因为标准输出是行缓冲的,意味着当遇到换行符或者程序正常结束时,才会刷新缓冲区。如果你在使用
printf
输出的内容没有包含\n
并且程序没有正常结束,可能会导致输出没有立即刷新到终端,而是留在了缓冲区中。这就可能导致你看不到输出,或者输出的顺序不符合你的期望。为了确保
printf
输出的即时刷新,你可以使用fflush(stdout)
函数,它可以强制将缓冲区的内容刷新到标准输出。
-
_exit
函数#include <unistd.h> void _exit(int status); 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
- 说明:虽然
status
是int
,但是仅有低8位(前面的位还有用来记录信号值)可以被父进程所用。所以_exit(-1)
时,在终端执行echo $?
发现返回值是255
。
- 说明:虽然
-
exit
函数#include stdlib.h> #include <unistd.h> void exit(int status);
-
说明:
exit
最后也会调用_exit
, 但在调用_exit
之前,还做了其他工作:-
执行用户通过 atexit或on_exit定义的清理函数。
-
关闭所有打开的流,所有的缓存数据均被写入(相当于带有刷新缓冲区的功能)。
-
调用_exit。
-
-
-
举例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ printf("进程退出咯"); //exit(0); _exit(0); }
-
return
退出:return
是一种更常见的退出进程方法。执行return n
等同于执行exit(n)
,因为调用main
的运行时函数会将main
的返回值当做exit
的参数。
-
2.3.3.2.、进程退出信号
- 进程退出信号是在status的低7位。我们获取进程退出代码可以使用对位与的方法,及
&0x7f
。
2.4、进程优先级
基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux或者unix系统中,用
ps –l
命令则会类似输出以下几个内容:注意到其中的几个重要信息:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
其中PRI 和 NI 是我们在这一节更需要注意的。
PRI(即priority)也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
NI 就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。注意,这里的PRI(old)在进程创建时就已经确定了,是不可改变的,即每次对NI的修改,都是在这个PRI(old)基础上进行修改的。
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
用top命令更改已存在进程的nice
- 在命令行输入
top
- 进入top后按“r”–>输入进程PID–>输入nice值。(如果权限不够就切换root身份(
su -
))#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(){ while(1){ printf("I am a process! pid: %d, ppid: %d\n",getpid(),getppid()); } return 0; }
一些概念:
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
2.5、进程程序替换
- 基本概念:
进程程序替换是指一个正在运行的进程被另一个程序替代的过程。这通常通过执行
exec
系列函数来实现。在进程程序替换发生时,原进程的地址空间、代码、数据和堆栈等内容会被新程序所取代,新程序接管原进程的执行(也就是不会创建新进程)。以下是进程程序替换的一般步骤和主要特点:
加载新程序: 新程序的可执行文件被加载到原进程的地址空间中。这包括新程序的代码段、数据段以及堆栈等。
更新进程上下文: 进程上下文包括寄存器的状态、程序计数器和堆栈指针等。这些上下文信息会被更新,以便开始执行新程序的代码。
关闭文件描述符: 进程通常会继承一些文件描述符,这些描述符可能是原进程打开的文件、套接字等。在进程替换时,可以选择关闭一些文件描述符,打开新的文件描述符,或者继续使用原有的描述符。
设置环境变量和命令行参数: 新程序可能需要特定的环境变量或命令行参数来正确执行。这些信息需要在进程替换前进行设置。
权限检查: 系统可能对新程序的执行权限进行检查,确保进程有权执行新程序。
执行新程序: 进程开始执行新程序的代码,原程序的执行被替代。
资源清理: 在进程替换完成后,可能需要进行资源清理工作,释放原进程占用的资源,如内存、文件描述符等。
进程程序替换的主要优点是在无需创建新进程的情况下,实现了程序的动态更新。这在一些场景下很有用,例如在服务器端动态加载新的应用程序或更新服务时。常见的
exec
函数有execve
、execvp
、execl
等,它们提供了不同的参数传递方式和替换策略。
- 常见的程序替换函数使用:
execl
:参数path需要指令的绝对路径,arg需要具体的指令,需要分开写,最后以NULL结尾。如execl("/usr/bin/ls", "ls" , "-a" , "-l", NULL);
。
execlp
:参数file只需要指令名称(绝对路径也可以),arg需要具体的指令,需要分开写,最后以NULL结尾。如execlp("ls", "ls" , "-a" , "-l", NULL);
。
execle
:给子进程设置全新的环境变量。参数path需要指令的绝对路径,arg需要具体的指令,需要分开写,最后以NULL结尾,envp需要自己设置环境变量数组。envp环境变量数组举例:
char *const env[] = { (char*)"Y=yyyyyy", (char*)"Z=zzzzzz", (char*)"L=llllll" };
如
execle("./mytest", "mytest" , NULL , env);
。
execv
:参数path需要指令的绝对路径,argv需要具体的指令数组。argv指令数组举例:
char *const argv[] = { (char*)"ls", (char*)"-l", (char*)"-a" };
如
execv("/usr/bin/ls", argv);
execvp
:参数file只需要指令名称(绝对路径也可以),argv需要具体的指令数组。如execvp("ls", argv);
。
process_replace.c
文件#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(){ printf("I am a process!,pid:%d\n",getpid()); char *const env[] = { (char*)"Y=yyyyyy", (char*)"Z=zzzzzz", (char*)"L=llllll" }; pid_t id = fork(); if(id == 0){ char *const argv[] = { (char*)"ls", (char*)"-l", (char*)"-a" }; printf("进程程序替换开始\n"); sleep(2); //带l的有可变参数,带p的路径可以只需要指令名称,带e的有环境变量 execle("./mytest","mytest","-a","-b","-c","-d", NULL,env); //execvp("ls", argv); //execv("/usr/bin/ls", argv); //execlp("ls", "ls", "-a", "-l", NULL); //execl("/usr/bin/ls", "ls", "-a", "-l", NULL); printf("进程程序替换结束\n"); } pid_t rid = waitpid(id,NULL,0); if(rid > 0){ printf("wait success\n"); } }
mytest.cc
文件#include <iostream> #include <stdio.h> int main(int argc, char* argv[], char* env[]){ for(int i=0; argv[i]; ++i){ printf("argv[%d]=%s\n",i,argv[i]); } for(int i=0; env[i]; ++i){ printf("env[%d]=%s\n",i,env[i]); } return 0; }
Makefile
文件.PHONY:all all:process_replace mytest mytest:mytest.cc g++ -o $@ $^ -std=c++11 process_replace:process_replace.c gcc -o $@ $^ .PHONY:clean clean: rm -f process_replace mytest
- 运行结果
- 助记:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
2.6、环境变量
基本概念:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录
~
)SHELL : 当前Shell,它的值通常是/bin/bash
查看环境变量方法:
echo $NAME
//NAME:你的环境变量名称这里所显示的路径环境变量是系统中的命令的路径,比如
ls
等。测试文件:
- 创建command_path.c文件
#include <stdio.h> int main(){ printf("测试指令路径\n"); return 0; }
对比./command_path执行和之间command_path执行区别
为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径(比如需要绝对路径
/home/xp2/Linux_Primary/day_21/command_path
或者./command_path
)才能执行?将我们的程序所在路径加入环境变量PATH当中,
export PATH=$PATH:command_path程序所在路径
。这样command_path
程序所在路径就是系统路径里了,下面执行command_path
程序就不用绝对路径了,直接在命令行输入command_path
即可。命令行输入对比
/home/xp2/Linux_Primary/day_21/command_path
或者./command_path
和command_path
。还有什么方法可以不用带路径,直接就可以运行呢?
使用符号链接:如果知道程序的相对路径或部分路径,可以将其链接到某个目录(例如
/usr/local/bin
),这样就可以从任何地方运行它。使用别名:使用
alias
命令可以为命令创建别名,这样可以使用更短或更易于记忆的名称来代替长命令。和环境变量相关的命令:
echo: 显示某个环境变量值
env: 显示所有环境变量
export: 设置一个新的环境变量
unset: 清除环境变量
set: 显示本地定义的shell变量和环境变量
环境变量组织方式:
- 每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
通过代码获取环境变量:
通过代码获取命令行参数:用到
main
函数第一个参数(命令行输入的字符串个数)和第二个参数(命令行输入的字符串,存到了argv数组中,以NULL结尾,和environ的组织方式类似)#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[], char *env[]) { if (argc != 4) { printf("usage:\n\t%s -[add|sub|mul|div] x y\n\n", argv[0]); return 1; } int x = atoi((const char*)argv[2]); int y = atoi((const char*)argv[3]); if (strcmp(argv[1], "-add") == 0) { printf("%d+%d = %d\n", x, y, x + y); } if (strcmp(argv[1], "-sub") == 0) { printf("%d-%d = %d\n", x, y, x - y); } if (strcmp(argv[1], "-mul") == 0) { printf("%d*%d = %d\n", x, y, x * y); } if (strcmp(argv[1], "-div") == 0) { printf("%d/%d = %d\n", x, y, x / y); } return 0; }
main
函数第三个参数#include <stdio.h> int main(int argc, char *argv[], char *env[]){ int i = 0; for(; env[i]; i++){ printf("%s\n", env[i]); } return 0; }
通过第三方变量
environ
获取#include <stdio.h> int main(int argc, char *argv[]){ extern char **environ; int i = 0; for(; environ[i]; i++){ printf("%s\n", environ[i]); } return 0; }
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
通过系统调用获取或设置环境变量:
putenv
:顾名思义,设置环境变量,但是请注意,使用putenv
设置的环境变量实际上是直接修改了当前进程的环境变量,因此这种更改在程序结束后不会保留。如果你想要在**调用其他程序时(这里不要搞混淆,子进程还是会继承父进程环境变量)**传递环境变量,可以考虑使用setenv
函数,它允许更灵活地设置和修改环境变量。以下是putenv
函数的基本用法:#include <stdlib.h> int putenv(char *string);
- 函数接受一个形如
"name=value"
的字符串,将其添加到当前进程的环境变量中。如果环境变量已经存在,则它的值将被替换。如果字符串中不包含等号(=
),则环境变量会被删除。
setenv
:setenv
函数用于设置环境变量,提供了更为直观和安全的方式,相对于putenv
更加现代和推荐使用。下面是setenv
函数的基本用法:#include <stdlib.h> int setenv(const char *name, const char *value, int overwrite);
name
: 要设置的环境变量的名称。value
: 要设置的环境变量的值。overwrite
: 一个整数,表示是否覆盖已存在的环境变量。如果overwrite
为非零值,将覆盖已存在的环境变量;如果为零值,不会覆盖已存在的环境变量。
getenv
:顾名思义,获取环境变量。以下是getenv
函数的基本用法:#include <stdlib.h> char *getenv(const char *name);
name
: 要获取值的环境变量的名称。getenv
函数返回一个指向以 null 结尾的字符串的指针,表示指定环境变量的值。如果环境变量不存在,则返回NULL
。#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ extern char **environ; char str[] = "HELLO=aaaaaaaaaaaaa"; putenv(str); setenv("BYEBYE","bbbbbbbbbbbbb",1); printf("%s\n",getenv("PATH")); printf("%s\n",getenv("HELLO")); printf("%s\n",getenv("BYEBYE")); int i = 0; for(; environ[i]; ++i){ printf("%s\n",environ[i]); } pid_t id = fork(); if(0 == id){ printf("我是子进程\n"); printf("%s\n",getenv("HELLO")); printf("%s\n",getenv("BYEBYE")); int i = 0; for(; environ[i]; ++i){ printf("%s\n",environ[i]); } } return 0; }
环境变量通常是具有全局属性的:
环境变量通常具有全局属性,可以被子进程继承下去。
命令行中输入export 环境变量(例如:
MYENV=mmmmmmmm
) 和 单纯输入环境变量(例如:MYENV=mmmmmmmm
) 区别:
- export 环境变量(例如:
MYENV=mmmmmmmm
) :这里是导出环境变量MYENV放到**普通变量(环境变量)**里,可以命令行输入env查看到。影响子进程:当你使用export
命令设置环境变量时,该环境变量将会影响当前 Shell 进程及其所有子进程。这意味着这个环境变量会传递给由当前 Shell 启动的其他命令或脚本。- 环境变量(例如:
MYENV=mmmmmmmm
):这里是设置的本地变量(局部变量),只能在当前 Shell 函数或脚本中可见(echo $MYENV
查看)。在函数或脚本的其他部分,以及在启动的子进程中,这些局部变量是不可见的。#include <stdio.h> #include <stdlib.h> int main(){ char* ret = getenv("MYENV"); if(ret){ printf("%s\n",ret); } return 0; }
2.6、进程地址空间
研究背景:
- kernel 2.6.32
- 32位平台
对比C语言内存空间布局:
- 程序地址空间和这个布局类似。
查看以下代码运行结果:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int g_val = 1; int main(){ pid_t id = fork(); if(id < 0){ perror("fork"); return 0; }else if(id == 0){ printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); }else{ printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } return 0; }
我们发现父进程和子进程里变量g_val的值一样,地址也一样。
再看下面修改g_val值的代码:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int g_val = 1; int main(){ pid_t id = fork(); if(id < 0){ perror("fork"); return 0; }else if(id == 0){ g_val = 100; printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); }else{ printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } return 0; }
我们发现父进程和子进程里变量g_val的值不同,但是地址相同,说明这里输出的变量不是同一个变量,只是变量名字一样而已!
这里我们就能想到,这个共同的地址肯定不是真实地址(物理地址),也就是说这个地址是虚拟地址。(其实这里也涉及到写时拷贝)
操作系统负责将虚拟地址转换为物理地址。
进程地址空间分布图:
- 通过这个分布图,我们可以清晰的看到,父子进程g_val的虚地址是一样的,但是父子进程维护不同的页表(简单来说就是虚拟地址映射为物理地址的映射表),也就是虚拟地址映射为物理地址的映射规则不同,因此映射到物理内存的父子进程的g_val的地址不一样,值也不同(如果父子进程都没对g_val进行修改,那么父子进程g_val物理内存也指向同一块空间,即在对变量进行修改的时候,需要进行写实拷贝,这时候就会对应不同的物理地址)。
2.7、Linux2.6内核进程调度队列
一般来说,一个CPU只有一个runqueue。如果有多个CPU,则需要考虑到进程个数的负载均衡问题。
活跃队列
时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级(因此优先级数值范围是0~139,其中0~99是实时优先级,100~139是普通优先级)!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
从0下表开始遍历queue[140]
找到第一个非空队列,该队列必定为优先级最高的队列
拿到选中队列的第一个进程,开始运行,调度完成!
遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活跃队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算(重新放到活跃队列运行)
active指针和expired指针
- active指针永远指向活跃队列
- expired指针永远指向过期队列
- 可是活跃队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
总结
- 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法。
3、自制Shell
效仿shell交互界面:
所以要写一个shell,需要循环以下过程
获取命令行
解析命令行
建立一个子进程(fork)
替换子进程(execvp)
父进程等待子进程退出(wait)
实现代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define SIZE 1024 #define MAX_LEN 128 #define SEP " " char *argv[MAX_LEN];//命令行字符串数组 char pwd[SIZE]; char envtemp[SIZE]; int lastcode = 0;//退出码 const char *HostName() { char *hostname = getenv("HOSTNAME"); if (hostname) return hostname; else return "None"; } const char *UserName() { char *hostname = getenv("USER"); if (hostname) return hostname; else return "None"; } const char *CurrentWorkDir() { char *hostname = getenv("PWD"); if (hostname) return hostname; else return "None"; } char *Home() { char *hostname = getenv("HOME"); if (hostname) return hostname; else return "None"; } int Interactive(char *commandline, int size) { printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); fgets(commandline, SIZE, stdin); commandline[strlen(commandline) - 1] = '\0'; return strlen(commandline);//空串返回0 } void Split(char *commandline) { int i = 0; argv[i++] = strtok(commandline, SEP); while (argv[i++] = strtok(NULL, SEP)); //解决ls无彩色问题 if (strcmp(argv[0], "ls") == 0) { argv[i - 1] = (char *) "--color"; argv[i] = NULL; } } int BuildingCmd() { int ret = 0; if (strcmp(argv[0], "cd") == 0) { ret = 1; char *target = argv[1]; //cd XXX 和cd if (!target) target = Home();//第二个参数为NULL //改变当前工作目录 chdir(target); //处理target为..的情况 //重新获取当前路径 char temp[1024]; getcwd(temp, 1024); //更新当前环境变量PWD snprintf(pwd, SIZE, "PWD=%s", temp); //导出环境变量 putenv(pwd); } else if (strcmp(argv[0], "export") == 0) { ret = 1; if (argv[1]) { strcpy(envtemp, argv[1]); putenv(envtemp); } } else if (strcmp(argv[0], "echo") == 0) { ret = 1; if (argv[1] == NULL) { printf("\n"); } else { if (argv[1][0] == '$') { if (argv[1][1] == '?') { printf("%d\n", lastcode); lastcode = 0; } else { char *e = getenv(argv[1] + 1); if (e) printf("%s\n", e); } } else { printf("%s\n", argv[1]); } } } return ret; } void Execute() { //只能交给子进程,如果用父进程执行命令行,执行一次就结束了 pid_t id = fork(); if (id < 0) perror("fork\n"); else if (id == 0) { execvp(argv[0], argv); exit(1);//执行完退出 } //父进程等待子进程 int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) lastcode = WEXITSTATUS(status);//等待成功 } int main() { while (1) { char commandline[SIZE]; //1. 打印命令行提示符,获取用户的命令字符串 int n = Interactive(commandline, SIZE); if (!n) continue;//返回值为0就是空串,下面代码不再执行 //2. 对命令行字符串进行切割 Split(commandline); //3. 处理内建命令 n = BuildingCmd(); if (n) continue; //4. 执行这个命令 Execute(); } //int i; //for(i=0;argv[i];++i){ // printf("argv[%d]:%s\n",i,argv[i]); //} return 0; }
那么好,Linux进程就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。
Xpccccc的github主页