这里是目录标题
vim批量化注释
注释:ctrl + v, hjkl选中区域,shift+i,然后按esc,再按// 就加上注释了。
去注释:小写,ctrl + v,选中区域,按d。
如何创建进程
fork函数的创建子进程
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程, pid:%d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
while(1)
{
printf("我是父进程, pid:%d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
}
for创建时,内核做了什么?
fork之后
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程: pid: %d\n", getpid());
fork();
printf("我依旧是一个进程:pid:%d\n", getpid());
sleep(1);
}
fork之后,是否只有fork之后的代码是被父子进程共享的?
进程具有独立性:代码和数据必须独立的。
代码只能读取。
写时拷贝。
那么所谓的代码共享到底是什么?
最准确的说法是:fork之后,父子共享所有的代码!意味着整个程序代码都会共享。
子进程执行的后续代码 不等于 共享的所有代码,只不过子进程只能从这里开始执行!
为什么呢?为什么子进程只能从这里执行?
因为cpu的寄有个叫做eip的程序计数器,他会保存当前正在执行指令的下一条指令。
eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处该是执行了!
结论就是:fork后代码是共享的,数据是以写时拷贝的方式进行拷贝的。
进程
fork之后
进程 = 内核的进程数据结构 + 进程的代码和数据
创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立!
写时拷贝
深浅拷贝就可以理解为写时拷贝。
前提就是写的时候再拷贝。
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
写时拷贝本身就是由OS的内存管理模块完成的。
优点:
1.只会拷贝父子修改的,变相的,就是拷贝数据的最小成本。拷贝的成本依旧存在。那么为什么要等到写的时候再拷贝?
2.延迟拷贝策略,只有真正的时候,才给你用!最大的意义在于:你想要,但是不立马使用的空间,先不给你,意味着可以先给别人用!
为什么要写时拷贝?
为什么不在创建进程的时候,就把数据分开不行吗?
这种方案本身来说是可以的。但是不是最优的。
为什么不选择它呢?
1.父进程的数据,子进程不一定全用。
比如全局变量,父进程定义了10个,子进程只需要用1个,剩下的就全浪费了空间。
2.即便10个全局变量都用,但不一定写入,可能只做读取,所以有浪费空间的嫌疑。
3.假如存在多个不需要修改的变量。那么还会造成空间浪费,不需要修改的数只需要存在一份即可。
最理想的情况,只有会被父子修改的数据,进行分离拷贝。不需要修改的共享即可。但是这种技术是不可实现的。
4.如果fork的时候,就无脑拷贝数据给子进程,无疑会增加fork()的成本。(内存和时间)
所以最终采用写时拷贝。
终止进程
常见的进程退出:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码没跑完,程序异常了
关于终止的正确认识
在写C语言时,经常会写return 0,
1.写return 0给谁return?
2.为何是0?其他值可以吗?
返回值为0,代表进程代码跑完,结果是否正确。
非0,代表失败。所以:非零标识不同的原因!
return的值表征了进程退出的信息。
将来这个退出信息是要给父进程进行读取的。
父进程根据这个退出码,来确定写的对不对。
关于终止的常见做法
1.在main函数中return,为什么其他函数不行?
2.在自己的代码任意地点中,调用exit(),代表进程退出。
exit()函数里面的参数也是退出码,代表各种信息。
_exit和exit
二者的区别在于:
1._exit直接终止进程,不会有任何刷新操作
2.exit终止进程,刷新缓存区
关于终止,内核做了什么
进程 = 内核结构 + 进程代码 和 数据
将进程设置为x状态,然后释放数据。
内核数据结构比如task_struct和mm_struct可能不会被释放。因为有内核的数据结构缓存池,叫做slab分派器。
echo $?
echo是内置命令,$?在bash中,代表得到最近一次执行完毕时,对应进程的退出码!
一般而言,失败的非零值该怎么设置,也就是返回值该怎么写,以及默认表达的含义。
进程等待
为什么要进行进程等待?
1、主要是为了解决内存泄漏的问题。防止进入僵尸状态。
2、获取子进程的退出状态
如何等待?
wait
1.父进程直接调用wait即可。
监控脚本,输入done即可运行
while : ; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; echo "---------------------------------"; sleep 1;
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//孩子
while(1)
{
printf("我是子进程,我正在运行......pid: %d\n",getpid());
sleep(1);
}
}
else{
printf("我是父进程:pid : %d 我准备等待子进程了\n", getpid());
sleep(20);
pid_t ret = wait(NULL);
if(ret <0)
{
printf("等待失败\n");
}
else{
printf("等待成功: result:%d\n",ret);
}
sleep(20);
}
}
wait()的方案可以解决回收子进程的Z状态,让子进程进入X状态。
waitpid
wait和waitpid都是系统调用,都是从子进程的task_struct中拿出子进程的退出码。
等待任意一个退出的子进程
pid_t waitpid(pid_t pid, int* status, int options);
参数列表:
1.pid大于0,是几,就代表等待哪一个子进程, pid = 123,指定等待123.
-1:等待任意进程。
2.options为0,代表阻塞等待。
3.status代表一个输出型参数。
什么是输出型参数?
输出型参数的意思就是通过调用该函数,从函数内部拿出特定的数据。也就是拿出来一个整数。
status只需要关心该整数的低16比特位,低16比特位会被分为三部分。status右移八位并不影响status本身的值。
次低8位,可以得到退出码。
最低八位,是异常状态,也就是异常退出。
返回值:
pid_t:
大于0 :等待子进程成功,返回值就是子进程的pid
小于0:等待失败
终止信号:
进程退出,如果异常退出,是因为这个进程收到了特定的信号!!
kill -l是Linux中的信号,后面需要学。没有0号信号!!
一旦进程出现异常,只关心退出信号,关注退出码毫无意义。
阻塞等待和非阻塞等待
waitpid()回收状态。
假如子进程就是不退出,则父进程只能进行阻塞等待。当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待,本质:就是当前进程变成阻塞状态,等条件就绪的时候,再被唤醒。
进程间的替换
为了让子进程执行一个全新的程序,所以才需要进程间替换。
什么是进程间的替换?
概念:让子进程执行新的程序
原理:
1.将磁盘中的程序,加载到内存结构中
2.重新建立页表映射,设执行程序替换,就重新建立谁的映射,结果:让我们父子进程彻底分离,并让子进程执行一个全新的程序。
为什么要进程间替换?
我们一般在服务器设计(Linux编程)的时候,往往需要子进程干两个种类事情。
1.让子进程执行父进程的代码片段(服务器代码)
2.让子进程执行磁盘中一个权限的程序(shell,想让客户端执行相应的程序,或者通过我们的进程,执行其他人写的进程代码)。
例如用C++调用其他人写的python、java等。
怎么编码?
父进程(单进程)execl简单替换
注意:如果一旦替换成功,是将当前进程的代码和数据全部替换了。execl这一行的下面的代码全部被替换了。
程序替换不需要判断返回值,因为只要替换成功,就不会有返回值,如果没有成功,就会执行下面的代码,执行后面的代码就会知道执行失败了。
1.找到要替换的程序路径
2.然后输入选项,以null结束。
子进程的替换
int execl(const char *path, const char *arg, ...);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
例如执行ls程序
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
简易shell制作
#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
char cmd[LEN]; //存储命令
char* myargv[NUM]; //存储命令拆分后的结果
char hostname[32]; //主机名
char pwd[128]; //当前目录
while (1){
//获取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//读取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '\0';
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //创建子进程执行命令
if (id == 0){
//child
execvp(myargv[0], myargv); //child进行程序替换
exit(1); //替换失败的退出码设置为1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0){
printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码
}
}
return 0;
}