做心理咨询可以在哪些网站发贴,1000元做网站,网站开发 pdf 文字版,网页设计与制作模版1. 进程创建
在这之前我们曾了解过进程创建#xff08;详见进程初识#xff08;二#xff09;#xff09;#xff0c;我们在这里对fork函数做一些补充 其实对于父子进程来说#xff0c;若是有一方试图修改数据时#xff0c;会向物理内存中申请一份新空间#xff0c;并…1. 进程创建
在这之前我们曾了解过进程创建详见进程初识二我们在这里对fork函数做一些补充 其实对于父子进程来说若是有一方试图修改数据时会向物理内存中申请一份新空间并将数据拷贝到其中拷贝完成后将自己对应页表中的只读属性去掉。
2. 进程终止
我们之前都知道在main函数的最后我们一般都有return 0;这个语句那么为什么要返回0呢返回12怎么样这个值返回给了谁以及为什么要返回这个值呢 在这里返回的这个0其实是程序的退出码来表征进程的运行结果是否正常0表示success。对于一个进程来说当它终止时无外乎三种情况 1. 代码运行完运行结果正确 2. 代码运行完运行结果不正确 3. 代码异常终止 对于前两种情况来说我们怎么知道运行结果是否正确呢——可以使用return 返回不同数字来表示不同的出错原因这就是进程的退出码。因此对于代码运行完后判断运行结果是否正确统一采用进程的退出码来判定。
那么除了代码运行完以外代码也有可能异常终止此时代码可能没有跑完因此在这里进程的退出码毫无意义即不关心退出码我们需要关注的是为什么异常发生了什么异常 在这个过程中先确定是否出现异常未出现就返回退出码而出现了异常操作系统会发出信号然后退出。
我们可以用如下方式来验证 对于上面这段代码我们运行可以发现 在kill指令中我们可以找到与之相对应的信号 我们对一个正常运行的进程使用kill -8 PID有 可以得到相同的结果。
main函数的返回值本质表示进程运行完成时是否是正确结果若不是可以用不同数字来表示不同的出错原因而对于进程来说谁最关心当前进程的情况呢——父进程因此main函数的返回值其实是返回给了父进程。而对于退出码我们可以使用
echo $?
来获取最近一次的退出码如 而在C语言的库里面有一个将错误码转换为错误信息的函数即strerror其手册如下 我们可以使用如下代码将所有错误信息打印出来 运行有 在之前我们ls一个不存在文件时有 可以看到这里返回的是错误码为2的错误信息我们获取错误码也有 即系统提供的错误码和错误码描述是有对应关系的当然我们也可以自己设计一份举个例子 而除了使用main函数以外我们还可以使用exit函数和_exit函数来退出进程如exit(0);它们与return的区别在于 exit函数在任意地方被调用都表示调用进程直接退出 return 只表示当前函数返回 而exit与_exit之间亦有差距_exit是系统调用而exit是用户函数他会在函数实现的过程中调用_exit对于下面这个代码 在使用exit时结果为 在使用_exit时结果为 在这里printf函数其实是先把数据写入到缓存区在合适的时候进程刷新exit属于用户函数它在实现时内部应该会调用一些函数进行冲刷缓存区的操作这之后再调用_exit而_exit则是系统层面直接将这个进程关闭因此也不会有冲刷其缓存区的情况。
3. 进程等待
①进程等待是什么 进程等待就是通过系统调用wait/waitpid来进行对子进程进行状态检测与回收的功能。 ②为什么需要进程等待
在之前我们曾经提到过僵尸进程的存在由于进程在变成僵尸进程后无法被杀死我们需要使用进程等待来解决内存泄漏问题这个问题必须解决。此外一个父进程需要通过进程等待进而获得子进程的退出情况这样做是为了知道父进程给子进程布置的任务完成地怎么样也有可能不关心。
③进程等待是怎么做的
在代码方面父进程通过调用wait/waitpid来解决僵尸进程问题我们可以查看手册有 在目前看来进程等待是必须的对于wait函数它是等待任意一个子进程退出如果子进程一直不退出父进程默认在wait调用这个系统调用的时候也就不返回此时就处于阻塞状态。对于waitpid函数它共有三个参数对于第一个参数如果传入的pid0时表示等待特定的子进程如果传入的pid-1时表示等待任意一个子进程第二个参数需要解释一下它是一个输出型参数即为了把值带出来这里的int是被当做几部分来使用的图解如下 前面我们知道父进程关心子进程那么父进程期望获得子进程退出后的哪些信息呢
1. 首先是子进程代码是否异常——对于status的0-7位来说当操作系统没有信号发出的时候默认都是0因此只有0-7位都是0就认为没有收到信号。
2. 没有异常发生那么结果对吗不对是因为什么——对于status的8-15位来说程序正常退出时默认为0即0-success若是结果不对则从其中读取不同的错误码。
举个例子status值为256时低16位为0000 0001 0000 0000此时程序正常退出退出码为1。我们可以使用以下代码来验证
#include sys/types.h
#include sys/wait.h
#include stdio.h
#include unistd.h
#include stdlib.h
#include string.hint main()
{pid_t id fork();if (id 0){perror(fork\n);exit(1);}else if (id 0){// childint cnt 5;while (cnt--){printf(this is child, pid:%d, ppid:%d, cnt:%d\n, getpid(), getppid(), cnt);sleep(1);}}else{// fatherint cnt 10;while (cnt--){printf(this is father, pid:%d, ppid:%d, cnt:%d\n, getpid(), getppid(), cnt);sleep(1);}// pid_t ret wait(NULL);int status 0;pid_t ret waitpid(id, status, 0);if (ret id){// 0x7f:0111 1111printf(wait success, ret:%d, exit sig:%d, exit code:%d\n, ret, status 0x7F, (status 8) 0xFF);}}return 0;
}运行有 既然如此那么进程等待原理是怎么样的呢 因为操作系统不会相信任何用户因此他会提供一个接口来让用户访问数据。此外操作提供提供了两个宏来供我们查看信息 WIFEXITED(status): 若为正常终止子进程返回的状态则为真。查看进程是否是正常退出 WEXITSTATUS(status): 若 WIFEXITED 非零提取子进程退出码。查看进程的退出码 之前我们的代码可以修改一部分即 而对于第三个参数options来说其一般默认为0即以阻塞方式等待除了0以外还可以传入一个参数——WNOHANGHANG意思是夯住指的是系统或进程在执行某个任务时变得非常慢或停滞导致系统或应用程序不再响应整体代表wait no HANG即等待的时候不要夯住举个例子来理解在一个男生等女友出门的时候WNOHANG表示的是男生每隔几分钟向女生打一次电话确认女生出门了没有而阻塞表示的是男生给女生打电话并且说不要挂电话等你出门了再挂。
④非阻塞轮询
在上面举的例子中男生打电话询问之后如果女友未出门未准备好也不进入阻塞状态再加上打电话的间隔时间就形成了非阻塞循环的形式我们将其称为非阻塞轮询。对于非阻塞轮询来说相对于阻塞最大的优势就是可以在这个等待的期间做一些自己的事情此时对于返回的ret来说当等待事件未就绪时就返回0。但是对于一个父进程来说当它处于非阻塞轮询的状态时等待子进程退出才是它的主要工作因此在此时能做的事只能是一些轻量化工作如打印日志等。我们可以定义如下的一些工作列表
#define TASK_NUM 10typedef void(*task_t)();
task_t tasks[TASK_NUM];void task1()
{printf(这是一个执行打印日志的任务, pid: %d\n, getpid());
}void task2()
{printf(这是一个执行检测网络健康状态的一个任务, pid: %d\n, getpid());
}void task3()
{printf(这是一个进行绘制图形界面的任务, pid: %d\n, getpid());
}int AddTask(task_t t);// 任务的管理代码
void InitTask()
{for(int i 0; i TASK_NUM; i) tasks[i] NULL;AddTask(task1);AddTask(task2);AddTask(task3);
}int AddTask(task_t t)
{int pos 0;for(; pos TASK_NUM; pos) {if(!tasks[pos]) break;}if(pos TASK_NUM) return -1;tasks[pos] t;return 0;
}void DelTask()
{}void CheckTask()
{}void UpdateTask()
{}void ExecuteTask()
{for(int i 0; i TASK_NUM; i){if(!tasks[i]) continue;tasks[i]();}
}
而我们可以在主函数代码中这样调用
int status 0;
InitTask();
while (1) // 轮询
{pid_t ret waitpid(id, status, WNOHANG); // 非阻塞if (ret 0){if (WIFEXITED(status)){printf(进程是正常跑完的, 退出码:%d\n, WEXITSTATUS(status));}else{printf(进程出异常了\n);}break;}else if (ret 0){printf(wait failed!\n);break;}else{ExecuteTask();usleep(500000);}
}这样封装也带来了非阻塞轮询和执行任务之间的解耦。
4. 进程程序替换
1. 单进程的进程程序替换
我们以下面这段代码为例
#include stdio.h
#include unistd.h
#include stdlib.hint main()
{printf(before: this is a process pid: %d, ppid: %d\n, getpid(), getppid());// 标准进程程序替换接口execl(usr/bin/ls, ls, -a, -l, NULL);printf(after: this is a process pid: %d, ppid: %d\n, getpid(), getppid());return 0;
}我们编译运行可以看到 在结果中我们可以看到before的打印成功了而after的打印未成功那么这究竟是怎么回事呢
2. 进程程序替换的原理 在一个正常运行的进程中各部分对应关系如下而当遇到exec*函数时会将exec函数中的一个参数中文件的代码与数据替换当前进程的代码与数据即 从这个基本原理我们可以看到整个过程没有创建新的进程同时也没有修改页表中的对应关系。
3. 多进程的进程程序替换
接下来我们使用多进程版来进行测试代码如下
#include stdio.h
#include unistd.h
#include stdlib.h
#include sys/types.h
#include sys/wait.hint main()
{pid_t id fork();if (id 0) // child{ printf(before: I am a process,pid: %d, ppid:%d\n,getpid(), getppid());//这类方法的标准写法//execl/usr/bin/ls“ls,a-l,NULL);execl( /usr/bin/top, top, NULL);printf(after: I am a process,pid: %d, ppid:%d\n, getpid(), getppid());exit(0);}// fatherpid_t ret waitpid(id, NULL, 0);if (ret 0) printf(wait success, father pid: %d,ret id: %d\n, getpid(), ret);return 0;
}在这里我们让子进程退出后可以看到 在子进程退出后父进程仍能对子进程进行进程等待由此我们可以得出结论——子进程的程序替换不会影响到父进程那么在这个过程中代码肯定发生了写实拷贝。在程序替换成功后exec*函数后的代码不会执行如果替换失败才可能执行后面的代码所以exec*函数只有失败的返回值而没有成功的返回值。
4. 多进程中验证exec*接口
我们使用man手册查看execl有 这6个程序替换的接口都提供加载器的效果即在shell中我们输入一条指令shell会创建一个新的进程并在其中调用exce*函数来加载指令。
①execl
首先先解释一下我们已经使用过的execl函数这里的l意为list
execl(/usr/bin/ls, ls, -a, -l, NULL);
我们要想执行一个程序第一件事应该是什么呢——先找到程序在哪因此在这个函数中传入的第一个参数就决定了如何找到这个程序后面的参数是命令行如何写就如何传参。在找到了程序后又干什么呢——根据传入的参数选项具体执行对应程序。
②execlp
然后我们介绍一下execlp函数l我们已经解释过这里的p意为PATHexeclp会在默认的PATH环境变量中查找指令我们可以用下面的代码验证
int main()
{printf(before: I am a process,pid: %d, ppid:%d\n,getpid(), getppid());execlp( ls, ls, -a, -l, NULL);printf(after: I am a process,pid: %d, ppid:%d\n, getpid(), getppid());return 0;
} ③execle
再然后我们介绍一下execle函数l不多说这里的e意为env即环境变量使用方式如下
extern char** environ;execle(/usr/bin/ls, ls, -a, -l, NULL, environ);
需要注意的是在这里传入自己所定义的环境变量采取的措施是覆盖而非追加。那么我们如何追加环境变量呢—— 我们可以调用脚本来将一个程序的环境变量导入到另一个程序中举个简单的例子 我们可以使用下面的代码来测试即
int main()
{printf(before: I am a process,pid: %d, ppid:%d\n,getpid(), getppid());execl( /usr/bin/bash, bash, shell.sh, NULL);printf(after: I am a process,pid: %d, ppid:%d\n, getpid(), getppid());return 0;
}看到这里我们能提出一个问题——为什么无论是可执行程序还是脚本都能跨语言调用呢其实所有语言运行的程序本质上都是进程。回归正题在了解了execle函数后我们若是想给子进程传递环境变量如何传呢 1. 新增我们可以使用putenv函数来给自己父进程添加环境变量 2. 彻底替换即使用execle函数来直接替换所有的环境变量 既然在这里谈到了我们之前讲过的环境变量那么我们可以思考一下环境变量是什么时候传入给进程的呢——我们要知道环境变量也是数据创建子进程的时候环境变量已经被子进程继承下去了。因此在程序替换中环境变量不会被替换的。
④execv
execv函数中的v意为vector它其实是一个指针数组我们以如下代码测试
int main()
{char* const myargv[] {ls,-a,-l,NULL};printf(before: I am a process,pid: %d, ppid:%d\n,getpid(), getppid());execv( /usr/bin/ls, myargv);printf(after: I am a process,pid: %d, ppid:%d\n, getpid(), getppid());return 0;
}
运行有 在这个例子中myargv是一个命令行参数而ls内部含有main函数这个main函数会去调用这个命令行参数去执行这样就能完成指定任务。后面的execvp, execvpe大同小异这里就不再赘述。
⑤execve
除了上面几个接口外还有一个execve函数其文档如下
它与前面六个函数的区别在于execve是系统调用前面六个函数都是库函数它们都要调用execve接口。