Linux进程控制

目录

写实拷贝

为什么要写实拷贝? 

fork函数 

返回值 

 常规用法

调用失败的原因

进程终止 

情况分类

a.代码正常执行完了

b.崩溃了(进程异常)

进程的退出码

c语言提供的系统的退出码

如何理解进程退出

 操作都有哪些方式?

main函数return。

exit函数退出

 进程等待

为什么要进程等待?

 什么是进程等待?

 进程等待的方法

 获取子进程status

父进程是如何获取子进程的退出信息的?

父进程在wait的时候,如果子进程没退出,父进程在干什么?

进程程序替换

替换原理

 替换函数

 int execl(const char *path, const char *arg, ...);

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

int execle(const char *path, const char *arg, ..., char *const envp[]); 

 int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]); 

int execve(const char *path, char *const argv[], char *const envp[]); 

函数解释

命名理解 

简易的shell


写实拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:

为什么要写实拷贝? 

操作系统不允许各种浪费或者不高效的行为存在

写实拷贝本质是一种按需申请资源的策略

fork函数 

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

进程调用fork,当控制转移到内核中的fork代码后,内核做:

分配新的内存块和内核数据结构给子进程

将父进程部分数据结构内容拷贝至子进程

添加子进程到系统进程列表当中

fork返回,开始调度器调度

返回值 

子进程返回0父进程返回的是子进程的pid

 常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。

一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

调用失败的原因

系统中有太多的进程

实际用户的进程数超过了限制

进程终止 

情况分类

a.代码正常执行完了
b.崩溃了(进程异常)

(例如野指针,访问越界等等)——崩溃的本质:进程因为某些原因,导致进程收到了来自操作系统的信号(kill-9)

进程的退出码

例如现在可以理解为什么之前的c语言都有一个return 0,这个0就是main函数的退出码,指的是程序执行成功,没有错误正常退出

可以根据进程的退出码判定进程是否正常退出

echo $?可以拿到当前进程的退出码

补充:$? 只会保存最近一次进程的退出码

c语言提供的系统的退出码

strerror()

退出码对应的描述

如何理解进程退出

OS内少了一个进程,OS就要释放进程对应的内核数据结构+代码和数据(如果有独立的)

 操作都有哪些方式?

main函数return

其它函数return呢? 仅仅代表函数返回->  进程执行,本质是main执行流执行

exit函数退出

 

这里也说明了 exit的参数就是进程的退出码

这个exit在函数的任意地方调用,都可以退出

还有一个接口叫 _exit() 的功能跟exit类似

但是 exit()会刷新输出缓冲区,_exit()不会刷新输出缓冲区

exit()

1. 执行用户通过 atexit或on_exit定义的清理函数。

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

 进程等待

为什么要进程等待?

1.避免内存泄漏(目前一定要做)

2.获取子进程执行的结果(如果必要的话)

之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。

另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

 什么是进程等待?

通过系统调用,获取子进程退出码或者退出信号的方式,顺便释放内存问题

 进程等待的方法

wait方法

返回值: 成功返回被等待进程pid,失败返回-1。

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
	pid_t id = fork();
	if(id == 0){
		//child
		int count = 10;
		while(count--){
			printf("我是子进程  PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	//父进程
	int status = 0;
	pid_t ret = wait(&status);
	if(ret > 0){
		//等待子进程退出
		printf("等待子进程成功\n");
		if(WIFEXITED(status)){
			printf("退出码:%d\n", WEXITSTATUS(status));
		}
	}
	sleep(3);
	return 0;
}

 

waitpid方法 

返回值:

正常返回的时候waitpid返回收集到的子进程的进程ID

如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

pid

Pid=-1,等待任一个子进程。与wait等效。

Pid>0.等待其进程ID与pid相等的子进程。

status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options:

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID

第二个参数

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回
 获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status(32位)不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):

 次低八位保存当前进程退出状态,低七位保存当前进程退出信号

 

 exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

对于此,系统当中提供了两个宏来获取退出码和退出信号。

    WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。

    WEXITSTATUS(status):用于获取进程的退出码。

exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码

父进程是如何获取子进程的退出信息的?

进程pcb会有相关的退出信息,操作系统将其设置到waitpid/wait的第二个参数中

父进程在wait的时候,如果子进程没退出,父进程在干什么?

附近在没有退出的时候,一直在调用waitpid进行等待——阻塞等待

进程程序替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

 

 替换函数

int execl(const char *path, const char *arg, ...);

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

nt execle(const char *path, const char *arg, ...,char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

-------------------------------------------------------------------------

以上都是对下面这个接口的封装,这个才是操作系统的替换函数

int execve(const char *path, char *const argv[], char *const envp[]);

 int execl(const char *path, const char *arg, ...);

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾

 例

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
 int execlp(const char *file, const char *arg, ...);

第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾

 例

execlp("ls", "ls", "-a", "-i", "-l", NULL);
int execle(const char *path, const char *arg, ..., char *const envp[]); 

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量

char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
 int execv(const char *path, char *const argv[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾

例 

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
int execvp(const char *file, char *const argv[]); 

第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾

例 

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
int execve(const char *path, char *const argv[], char *const envp[]); 

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量

例 

char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);
函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解 

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

简易的shell

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAX 1024
#define ARGC 64
#define SEP " "

int split(char *commandstr, char *argv[])
{
    assert(commandstr);
    assert(argv);

    argv[0] = strtok(commandstr, SEP);
    if(argv[0] == NULL) return -1;
    int i = 1;
    while((argv[i++] = strtok(NULL, SEP)));
    //int i = 1;
    //while(1)
    //{
    //    argv[i] = strtok(NULL, SEP);
    //    if(argv[i] == NULL) break;
    //    i++;
    //}
    return 0;
}

void debugPrint(char *argv[])
{
    for(int i = 0; argv[i]; i++)
    {
        printf("%d: %s\n", i, argv[i]);
    }
}

int main()
{
    while(1)
    {
        char commandstr[MAX] = {0};
        char *argv[ARGC] = {NULL};
        printf("[zhangsan@mymachine currpath]# ");
        fflush(stdout);
        char *s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s);
        (void)s; // 保证在release方式发布的时候,因为去掉assert了,所以s就没有被使用,而带来的编译告警, 什么都没做,但是充当一次使用
        // abcd\n\0
        commandstr[strlen(commandstr)-1] = '\0';
        // "ls -a -l" -> "ls" "-a" "-l"
        int n = split(commandstr, argv);
        if(n != 0) continue;
        //debugPrint(argv);
        // version 1
        pid_t id = fork();
        assert(id >= 0);
        (void)id;

        if(id == 0)
        {
            //child
            execvp(argv[0], argv);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
        //printf("%s\n", commandstr);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酷帅且洋仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值