海洋公园网站建设方案,有人用wordpress做企业,如何做自己的网站后台,线上软装设计师文章目录 Linux进程1、计算机体系结构和操作系统管理1.1、计算机体系结构 -- 硬件1.2、操作系统#xff08;Operator System#xff09; -- 软件 2、进程2.1、进程基本概念2.2、进程标识符2.2.1、获取当前进程标识符和当前进程的父进程标识符2.2.2、通过系统调用创建进程 -- … 文章目录 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进程控制块 程序。 进程信息被放在一个叫做进程控制块的数据结构中可以理解为进程属性的集合。课本上称之为PCBprocess control block。 //举例
struct PCB{//状态//优先级//内存指针//标识符//...包含进程几乎所有的属性字段struct PCB* next;
}Linux操作系统下的PCB是: task_struct task_ struct内容分类 标示符: 描述本进程的唯一标示符用来区别其他进程。状态: 任务状态退出代码退出信号等。优先级: 相对于其他进程的优先级。程序计数器: 程序中即将被执行的下一条指令的地址。内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子要加图CPU寄存器]。IO状态信息: 包括显示的I/O请求,分配给进程的IO设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。其他信息 查看进程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.hint main()
{while(1){sleep(1);}return 0;
}2.2、进程标识符 进程idPID 父进程idPPID 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.hint 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.hint 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.hint 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.hint 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.hint 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方法 #includesys/types.h
#includesys/wait.hpid_t wait(int *status);返回值成功返回被等待进程pid失败返回-1。
参数输出型参数获取子进程退出状态,不关心则可以设置成为NULLwaitpid方法 pid_t waitpid(pid_t pid, int *status, int options);返回值当正常返回的时候waitpid返回收集到的子进程的进程ID如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数pidPid-1,等待任一个子进程。与wait等效。Pid0.等待其进程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 i0; 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,status0x7F); // 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.hint 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*)Yyyyyyy, (char*)Zzzzzzz, (char*)Lllllll };如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.hint main(){printf(I am a process!,pid:%d\n,getpid());char *const env[] { (char*)Yyyyyyy, (char*)Zzzzzzz, (char*)Lllllll };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 i0; argv[i]; i){ printf(argv[%d]%s\n,i,argv[i]); } for(int i0; 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 $ $^ -stdc11 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.hint putenv(char *string);函数接受一个形如 namevalue 的字符串将其添加到当前进程的环境变量中。如果环境变量已经存在则它的值将被替换。如果字符串中不包含等号则环境变量会被删除。 setenv setenv函数用于设置环境变量提供了更为直观和安全的方式相对于 putenv 更加现代和推荐使用。下面是 setenv 函数的基本用法 #include stdlib.hint setenv(const char *name, const char *value, int overwrite);name: 要设置的环境变量的名称。value: 要设置的环境变量的值。overwrite: 一个整数表示是否覆盖已存在的环境变量。如果 overwrite 为非零值将覆盖已存在的环境变量如果为零值不会覆盖已存在的环境变量。 getenv顾名思义获取环境变量。以下是 getenv 函数的基本用法 #include stdlib.hchar *getenv(const char *name);name: 要获取值的环境变量的名称。getenv 函数返回一个指向以 null 结尾的字符串的指针表示指定环境变量的值。如果环境变量不存在则返回 NULL。 #include stdio.h
#include stdlib.h
#include unistd.h int main(){ extern char **environ; char str[] HELLOaaaaaaaaaaaaa; 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 环境变量(例如MYENVmmmmmmmm) 和 单纯输入环境变量(例如MYENVmmmmmmmm) 区别 export 环境变量(例如MYENVmmmmmmmm) 这里是导出环境变量MYENV放到**普通变量环境变量**里可以命令行输入env查看到。影响子进程当你使用 export 命令设置环境变量时该环境变量将会影响当前 Shell 进程及其所有子进程。这意味着这个环境变量会传递给由当前 Shell 启动的其他命令或脚本。环境变量(例如MYENVmmmmmmmm)这里是设置的本地变量局部变量只能在当前 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.3232位平台 对比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规则进行排队调度,所以数组下标就是优先级因此优先级数值范围是0139其中099是实时优先级100139是普通优先级 从该结构中选择一个最合适的进程过程是怎么的呢 从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 和cdif (!target) target Home();//第二个参数为NULL//改变当前工作目录chdir(target);//处理target为..的情况//重新获取当前路径char temp[1024];getcwd(temp, 1024);//更新当前环境变量PWDsnprintf(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(i0;argv[i];i){// printf(argv[%d]:%s\n,i,argv[i]);//}return 0;
}那么好Linux进程就到这里如果你对Linux和C也感兴趣的话可以看看我的主页哦。下面是我的github主页里面记录了我的学习代码和leetcode的一些题的题解有兴趣的可以看看。
Xpccccc的github主页