• Linux进程控制详解.


    🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
    在这里插入图片描述

    一、进程创建

    #include 
    pid_t id = fork();
    
    • 1
    • 2

    1.创建子进程,OS做的事

    进程=内核数据结构+进程数据和代码

    1. 分配新的内存块和数据结构给子进程
    2. 将父进程部分数据和内容进行拷贝
    3. 添加子进程到系统进程列表
    4. fork()返回,开始调度器调度

    理论上,子进程也要拥有自己的代码和数据,但是一般而言,子进程没有 加载的过程,所以,子进程没有自己的代码和数据,只有靠父进程

    2.写时拷贝

    代码是不可被写的,只能读取,所以父子可以共享
    数据是可被修改的,所以必须分开(修改的时候才进行)

    如果说创建进程的时候,就直接拷贝父进程的数据,那么可能子进程根本不会去用或者只是去读取,浪费空间。
    1.因为即便是OS也无法提前知道哪些空间会被提前写入,所以无法提前拷贝会被写入的那些空间
    2.提前拷贝了,会立马使用吗?
    所以OS选择了写时拷贝技术来实现父子进程数据的分离

    1.用的时候再分配是高效使用内存的表现
    2.操作系统无法预知被访问的空间

    因为有写时拷贝的存在,所以,父子进程得以彻底分开,保证了进程的独立性。写时拷贝是一种延迟申请的技术,可以提高整机内存的使用率。

    3.上下文数据

    创建子进程之后,父子进程是所有代码共享的(不仅仅是fork()之后的代码)

    进程随时可能被中断,下次回来因为必须从之前的位置继续执行,就要求CPU随时记录当前进程执行的位置,所以,CPU内由对应的寄存器数据,用来记录当前进程的执行位置,这个寄存器叫EIP(也叫做PC,程序计数器),记录的是下一条执行的代码,还有其他寄存器来记录其他状态,这些寄存器中的数据,我们叫做进程的上下文数据,创建的时候,上下文数据也是要给子进程的,所以这是的执行状态,父子进程相同,才觉得子进程只有fork()之下的代码,其实是全部代码

    4.fork调用失败的原因

    1.系统中已经有太多的进程
    2.实际用户的进程数超过了限制(一般的用户不会允许创建太多的进程)

    二、进程终止

    进程终止,OS释放进程申请的相关内核数据结构和对应的代码和数据
    进程终止的常见方式

    a.代码跑完,结果正确
    b.代码跑完,结果不正确
    c.代码没有跑完,程序崩溃

    1.main函数的返回值

    main函数的返回值我们总是写为return 0;但是其实他是有意义的,main函数的返回值并不总是0,可以是其他值,main函数的返回值叫做进程退出码,如果进程退出码为0(success),表示代码结果是正确的,不正确就是非0,main的返回值是用来返回给上一级进程的,用来评判该进程执行结果用的,我们可以忽略

    echo $?  来查看最近一次进程执行完毕之后的退出码
    
    • 1

    在这里插入图片描述
    在这里插入图片描述
    由上所述,直接返回0其实是不对的,应该对于函数中的执行结果设计不同的返回值,通过进程退出码来判定main函数执行结果的正确性(非零值有无数个,不同的非零值就可以表示不同的原因,方便定位错误的原因细节)

    2.strerror

    将进程退出码的含义进行打印
    在这里插入图片描述

    printf(strerror(num));
    //用这个来查看每个进程退出码所代表的函数,写个for循环
    
    • 1
    • 2

    在这里插入图片描述
    程序崩溃的时候,退出码无意义!因为退出码对应的return没有执行

    exit && _exit

    exit

    a.return语句,就是终止进程的,但是只有在main函数才是终止进程,其他函数是 函数返回。
    b.exit

    #include 
    int exit(int status)
    
    • 1
    • 2

    引起正常进程终止,能达到和return一样的效果,但是,exit在任何地方都可以调用,都表示终止进程,所以,如果想终止进程,推荐用exit

    _exit

    除了exit外,还有_exit,这是一个系统调用接口
    在这里插入图片描述

    区别

    exit:是一个库函数
    1.执行用户定义的清理函数
    2.冲刷缓冲,关闭流等
    3.进程退出

    _exit:是一个系统调用
    直接进程退出

    这里也可以看出来,缓冲区不再操作系统内部,另外,缓冲区是C标准库维护的

    三、进程等待

    1.wait

    在这里插入图片描述
    等待进程状态变化,wait是阻塞式的等,一直等到子进程状态变化

    #include 
    #include 
    pid_t wait(int*status)
    //status:输出型参数,不关心子进程退出结果则status为NULL
    //					关心子进程退出结果则status为&status
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #include 
    #include 
    #include 
    #include 
    
    #include 
    #include 
    int main()
    {
    	pid_t id = fork();
    	if(id<0)
    	{
    		perror("fork");
    		exit(1);//退出码为1,不正确
    	}
    	else if(id==0)
    	{
    		int cnt=5;
    		while(cnt)
    		{
    			printf("cnt: %d,im child process: pid:%d, ppid:%d\n",
    			cnt,getpid(),getppid());
    			sleep(1);
    			cnt--;
    		}
    		exit(0);
    	}
    	else
    	{
    		//父
    		printf("im parent process:pid:%d, ppid:%d\n",
    		getpid(),getppid());
    		sleep(7);
    		pid_t ret =wait(NULL);
    		if(ret>0)
    		{
    			printf("等待子进程成功,ret:,%d\n",ret);
    		}
    		while(1)
    		{
    			printf("im parent process: pid:%d,ppid:%d\n",
    			getpid(),getppid());
    			sleep(1);
    		}
    	}
    	
    	return 0;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在这里插入图片描述
    可以看出,父进程一直在等,直到子进程有了结果才继续运行

    2.waipid

    #include 
    #include 
    pid_t waitpid(pid_t pid,int*status,int options);
    //pid =-1,等任意一个子进程
    //waitpid(pid,NULL,0) 等于 wait(NULL)
    //pid>0,等待进程ID与pid相等的子进程
    //options默认为0,表示阻塞等待
    //status:输出型参数,不关心子进程退出结果则status为NULL
    //					关心子进程退出结果则status为&status
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    status的构成
    status并不是按照整数来整体使用的,而是按照bit位的方式,将32个比特位进行划分,只需要知道 低16位即可

    我们kill -l知道 是没有0号信号的
    在这里插入图片描述
    进程异常退出 就是操纵系统以发送信号的方式杀掉了该进程
    status的低7位是表示进程收到的信号(无0信号,0说明 是正常跑完的,运行期间没有收到信号)

    status的次低8位才是子进程退出码(异常退出进程退出码无意义)

    (status>>8)&OxFF//第八位,F要用四个bit位来表示(1111),所以低八位&0xFF
    
    • 1
    int status;
    pid_t ret = waitpid(id,&status,0);
    printf("等待子进程成功,ret:%d,子进程退出码:%d  \n",ret,(status>>8)&0xFF);
    //ret是wait/waitpid的返回值,正常退出进程退出码才有意义,程序崩溃的话进程退出码
    //是没有意义的。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.wait/waitpid凭什么拿到子进程退出码

    僵尸进程:是一个已死的进程,可以释放数据,至少要保留该进程的PCB信息,task_struct里面保留了任何进程退出时的退出信息,wait/waitpid就是读取子进程的task_struct结构。

    //内核源代码task_struct中我们可以看到两个字段
    int exit_code,exit_signal;//退出码和退出信号
    
    • 1
    • 2

    task_strcut是内核数据结构,而wait/waitpid是系统调用,当然有权力读取到

    4. waitpid参数构成

    1.pid
    id>0:等待指定进程退出
    id==-1:等待任意进程退出
    (wait其实是waitpid的子集)

    pid_t ret = wait(NULL);
    pid_t ret = wait(id/*子进程pid,创建子进程时的返回值*/,NULL,0/*阻塞等待*/);
    
    • 1
    • 2

    2.status
    对于status来说,还要自己去位运算,是不是太麻烦了,所以系统提供了宏

    WIFEXITED(statis)查看进程是否正常退出
    WEXITSTATUS(status)查看进程的退出码
    
    • 1
    • 2

    3.options
    默认是0,是阻塞等待
    阻塞的时候,父进程是什么都不敢的,如果想让他做点事呢?所以 可以将options设置为WNOHUNG,父进程就是非阻塞等待

    5.阻塞等待&&非阻塞等待

    阻塞等待,父进程后面的代码不执行,当条件满足的时候,EIP寄存器(PC指针)指向的下一行代码开始唤醒。

    非阻塞等待如果父进程检测子进程的退出状态,发现子进程没有退出,通过调用waitpid来等待,如果子进程没有退出,waitpid立马返回(阻塞等待在等待完成后才返回)

    非阻塞等待,标志位是WNOHANG(宏),子进程没退出就直接return,直接后面的代码,通过 多次调用waitpid来完成(基于非阻塞调用的轮询检测方案),面对IO的东西太多,用阻塞等待的话,系统就太慢了

    四、进程程序替换

    程序替换不是所有的代码和数据都被替换掉,比如环境变量就没有被进行替换。(可以理解为,和系统相关的东西没有被替换,无关的都被替换掉)
    子进程不想和父进程共享代码,想执行一段自己的代码–>进程程序替换
    程序替换是通过特定的机构,加载磁盘上的一个全新的程序(数据和代码),加载到调用进程的地址空间中
    在这里插入图片描述

    1.进程程序替换操作 exec系列函数

    execl

    int execl(const char*path,const char*arg,...);
    //可以想象成list,传参的时候传一串
    
    • 1
    • 2

    第一个参数是找到程序
    后面是可变参数列表,命令行上怎么执行,这里就怎么填
    并且最后一个参数必须以NULL结尾,表示参数传递完毕

    不创建子进程

    在这里插入图片描述

    int main()
    {
    	printf("当前进程的开始代码\n");
    	//execl("/usr/bin/ls","ls","-l","-a",NULL);
    	execl("/usr/bin/top","top",NULL);
    	printf("当前进程的结束代码\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    可以看到代码已经被替换,后面的代码不再执行
    在这里插入图片描述
    ls是带自由配色选项的,我们也可以加上这个选项 --color=auto

    创建子进程

    创建子进程 ,我们就可以替换子进程而不影响父进程(进程具有独立性)。我能需要让父进程聚焦在读取数据,解析数据,指派进程执行代码(子进程)的功能上

    execv

    int execv()const char*path,char*const argv[]);
    //可以想象成vector
    
    • 1
    • 2

    execl和execv本质上没有区别,只是在传参方式上有所不同

    #define NUM 16
    ...
    char*const _argv[NUM]={"ls","-a","-l",NULL};
    ...
    execv("/usr/bin/ls",_argv);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    execlp

    int execlp(const char*file,const char*arg,...);
    
    • 1

    exec系列函数,命名中带p的就是在环境中PATH中进行查找

    execlp("ls"/*在Path中找到ls*/,"ls","-a","-l",NULL);
    
    • 1

    execle

    int execle(const char*path,const char*arg,...,char*const  envp[]);
    //e表示环境变量,最后这个参数不传也是允许的
    
    • 1
    • 2
    #define NUM 16
    char*_env[NUM]={(char*)"MY_VAL=888777666555","NULL"};
    execle(path,参数,参数...,_env/*可不传*/);
    
    • 1
    • 2
    • 3

    2.如何替换我自己写的程序

    const char*mypath = "XXX路径";
    //可以从工作目录开始,也可以从当前目录开始
    ...
    execl("mypath","mycmd"/*自己写的程序*/,"-a"NULL);
    //-a参数,main是可以接收参数的
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.execve

    在这里插入图片描述
    上面6个exec系列函数底层是调用的同一个接口,为了满足不同的调用场景而实现了不同的封装,execve才是真正的系统调用接口

    在这里插入图片描述

  • 相关阅读:
    【JavaScript 逆向】猿人学 web 第二十题:新年挑战
    低代码维格云甘特视图入门教程
    Jmeter介绍以及脚本制作与调试
    java项目中数据权限实现思路
    QT基础教程之九Qt文件系统
    Mysql常见指令以及用法(保姆级)
    springboot毕设项目大学实验中心教学管理系统的设计马实现 yabss(java+VUE+Mybatis+Maven+Mysql)
    GitHub还能这样玩,这次我真是开了眼了
    Jekyll 选项(options)和子命令(subcommand)小手册
    【JavaSE】面试01
  • 原文地址:https://blog.csdn.net/zhu_pi_xx/article/details/127314209