Linux进程控制

进程创建:

fork()函数初识:

作用:在当前进程创建一个子进程,新进程称为子进程,原进程称为父进程

返回值:pid_t实际是一个int类型,typedef过,子进程返回0,父进程返回子进程id,出错返回-1

当fork()后内核做了什么 ?

1.分配新的内存块和数据结构(PCB,页表,进程地址空间)给子进程

2.将父进程的部分数据结构内容拷贝给子进程(继承)

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

4.fork()返回,开始调度器调用,调度器可能会选择父进程继续执行,也可能会选择子进程执行,这取决于调度算法和系统的当前负载情况。

注意:在现代操作系统中,fork() 调用后父进程和子进程会共享代码段。代码段是程序的可执行指令部分,由于代码在运行期间通常不会被修改,所以让父进程和子进程共享代码段可以节省内存资源。当 fork() 被调用时,内核不会为子进程复制一份代码段,而是让子进程的代码段指针指向与父进程相同的物理内存区域。

fork返回值问题???

1.如何理解fork函数有两个返回值问题?(换句话说也就是为什么一个函数能有两个返回值)

pid_t fork(void)
{
  //创建子进程PCB
  //进程地址空间
  // 等等
  return id;
}

理解:对于一个函数,在return之前已经执行完核心代码了,也就是你fork之后,跳到fork的函数执行,return之前已经创建好子进程了,所以return之前已经有了两个进程,父和子各自执行return,所以就有两个函数返回值

2.如何理解给父进程返回子进程id,给子返回0

因为父亲有且只能有一个,孩子可能有多个,所以父亲有唯一性,所以可以通过不同的id找不同的孩子

3.如何理解同一个变量pid有两个值让if else同时执行

先理解写时拷贝:

当发生对共享数据的修改时,就会发生写时拷贝,以保证进程的独立性(上一节有讲解),也就是在开辟空间,然后修改映射关系,在进程地址空间中地址是一样的,只是映射后的位置不一样了

所以为什么一个变量pid会有两个值,当你return后,也就是返回,返回的本质就是写入,所以谁先返回就把谁的id写入变量pid中,因为进程具有独立性,当写入第二个时就会发生写时拷贝,所以就可以根据pid映射到物理内存的不同,从而执行不同的执行流

fork()的常规用法

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

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

fork()调用失败的原因

1.系统中有太多的进程

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

进程终止:

为什么我们main函数后面有个return 0;这是进程退出时对于的退出码,我们可以通过退出码来判断进程执行的结果是否正确,一般用0表示成功,非0表示错误(不同的值对应不同的错误)

查看进程退出码:

echo $?    ?是一个环境变量,永远记录最近一个前台进程在命令行中执行完毕时对于的退出码

我们可以看到当你ls一个不存在的文件时,就会出错,查看进程退出码是2(稍后有讲解) 

进程退出场景

1.代码运行完毕,结果正确 return 0

2.代码运行完毕,结果不正确 return !0 //退出码这时候起作用,可以自己定义,也可以用系统的

3,代码异常终止   //此时退出码毫无意义(后面有讲解,在status详解那里)

    像什么除0 ,野指针越界等问题,kill -l可以看到各个退出码对应的意思

11为野指针,8为除0 

进程常见的退出方法:

正常终止:

1.从main函数返回

2.调用exit

3._exit(系统调用接口)exit和_exit后面会做讲解

异常终止:

ctrl+c,信号终止

exit:无论在哪里调用都表示终止进程,后续什么代码都不执行了,status表示当前函数的退出码

_exit

两个都是终止进程,_exit是系统调用接口,exit是库函数,但exit会在终止后刷新缓冲区,_exit不会,也就是exit是在_exit的功能基础上在添加一些。

 进程等待:

僵尸状态:之前讲过,子进程退出,如果父进程不管不顾,就可以造成僵尸进程问题,进而导致内存泄漏,当进程进入僵尸状态时,kill -9也没有作用

最后我们也想知道子进程完成任务的情况,所以可以通过进程等待的方式,回收子进程资源,获取子进程的退出信息,解决kill -9问题和内存泄漏问题以及获得子进程完成情况

进程等待的方法

wait方法:返回值,如果调用成功,返回子进程的id,通过这个id,父可以知道哪个子,如果没有子进程或在等待中出现错误,返回-1 (可能你执行程序时父进程按了ctrl+c,就会中断,然后返回-1)(status 是一个整型指针,用于存储子进程的退出状态信息不关心子进程退出码可以把status设为NULL)

waitpid方法:传谁的pid就等待谁,status 是一个整型指针,用于存储子进程的退出状态信息。如果不需要获取子进程的退出状态,可以将其设置为 NULL。通过一些宏可以对 status 指向的值进行解析,options默认设0表示阻塞式等待,返回值与wait一样

深刻理解status: (重点)

对于status(一般是16位的整形):我们只关心它的低16位

 

 前面说了一个进程终止有三种情况

1:代码执行完,结果正不正确,终止信号都为0

2.异常退出时,信号为其他(kill -l可以查看),信号异常也就是不为0就不关心退出码了,当信号为0时才关心退出码。

所以我们可以

pid_t ret=waitpid(id,&status,0);
printf("wait success:%d,exit_signal:%d,exit_code:%d"
,ret
,(status&0x7F) //按位与拿到低八位为信号
,(status>>7)&0xFF); // 右移8位再按位与拿到退出码
                                  
                                 

父进程如何获取退出信号和退出码,当waitpid时,父进程会去子进程的PCB中(exit_code和exit_signal都在子进程的PCB中)获取,然后放到status中

//系统提供了两个宏函数
if(WIFEXITED(status))//判断信号是否为0,为0则返回true
{
  printf("exit_code:%d",WEXITSTATUS(status));//获取退出码

}
 If  status  is  not  NULL,  wait()  and  waitpid() store status information in the int to which it points.  This integer can be inspected with the following macros (which take the integer itself as an argument, not a
       pointer to it, as is done in wait() and waitpid()!):

       WIFEXITED(status)
              returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main().

       WEXITSTATUS(status)
              returns the exit status of the child.  This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argument for a  return  statement
              in main().  This macro should be employed only if WIFEXITED returned true.

       WIFSIGNALED(status)
              returns true if the child process was terminated by a signal.

       WTERMSIG(status)
              returns the number of the signal that caused the child process to terminate.  This macro should be employed only if WIFSIGNALED returned true.

       WCOREDUMP(status)
              returns  true if the child produced a core dump.  This macro should be employed only if WIFSIGNALED returned true.  This macro is not specified in POSIX.1-2001 and is not available on some UNIX implementations
              (e.g., AIX, SunOS).  Only use this enclosed in #ifdef WCOREDUMP ... #endif.

       WIFSTOPPED(status)
              returns true if the child process was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or when the child is being traced (see ptrace(2)).

       WSTOPSIG(status)
              returns the number of the signal which caused the child to stop.  This macro should be employed only if WIFSTOPPED returned true.

       WIFCONTINUED(status)
              (since Linux 2.6.10) returns true if the child process was resumed by delivery of SIGCONT.

深刻理解options: 

 The value of options is an OR of zero or more of the following constants:

       WNOHANG     return immediately if no child has exited.

       WUNTRACED   also return if a child has stopped (but not traced via ptrace(2)).  Status for traced children which have stopped is provided even if this option is not specified.

       WCONTINUED (since Linux 2.6.10)
                   also return if a stopped child has been resumed by delivery of SIGCONT.

0表示阻塞式等待:

也就是父进程会停在wait/waitpid的地方,不会执行下面的代码,直到指定的子进程状态发生改变(正常退出、被信号终止、被暂停等)如果没有子进程或者等待子进程时失败

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

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        sleep(2);
        _exit(0);
    } else if (pid > 0) {
        // 父进程
        int status;
        pid_t ret = waitpid(pid, &status, 0);
        if (ret > 0) {
            printf("子进程 %d 状态已改变\n", ret);
        } else {
            perror("waitpid 调用失败");
        }
    }
    return 0;
}

 注意:可以通过perror打印调用失败

WNOHANG:非阻塞式等待(当设置为非阻塞状态时,waitpid有一个特殊的返回值,即0)

子进程状态未改变:若指定的子进程状态没有发生改变,waitpid 返回 0。这意味着父进程可以继续执行其他任务,后续可以再次调用 waitpid 来检查子进程状态。

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

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        sleep(5);
        _exit(0);
    } else if (pid > 0) {
        // 父进程
        int status;
        pid_t ret;
        do {
            ret = waitpid(pid, &status, WNOHANG);
            if (ret == 0) {
                printf("子进程还在运行,继续做其他事情...\n");
                sleep(1);
            }
        } while (ret == 0);
        if (ret > 0) {
            printf("子进程 %d 状态发生改变\n", ret);
        } else {
            perror("waitpid 调用失败");
        }
    }
    return 0;
}

如果没有while 循环,可能会造成父进程执行完下面的代码后先退出

综上所述,options 为 0 和 WNOHANG 时,waitpid 的返回值有所不同,特别是 WNOHANG 选项引入了返回 0 来表示子进程状态未改变的情况,使得父进程可以在不阻塞的情况下进行其他操作。

进程程序替换:

创建子进程的目的:
1.想让子进程执行父进程的一部分 (执行父进程对应的磁盘中的代码的一部分)                            2.想让子进程执行一个全新的程序(让子进程想办法加载磁盘上指定的程序,然后执行新的程序的   代码和数据,这就叫进程程序替换

理解原理 
本质就是指定程序的代码和数据加载到指定的位置(覆盖掉自己的代码和数据,上面这个图就表示子进程自己的PCB,其中的原来的代码和数据被另一个程序替换了),所以进程替换并没有创建新的进程,还是原来那个子进程,只是代码和数据替换了

替换函数(重点) 

其中有六种以exec开头的函数,统称为exec函数

execl函数,第一个参数path表示路径(也就是怎么找到程序),第二个参数是怎么执行,省略号是可变参数列表(可以不管),注意:所有的exec函数传参结束时以NULL结束

你可以这样调用execl("/usr/bin/ls","ls","-a","-l",NULL); 

注意:如果execl传参失败(也就是没有替换成功),那代码和数据就没有被覆盖,execl以下的原来的代码和数据还是会执行的

为什么execl没有成功返回值???

因为你即使返回也没用,后面的代码和数据都没用了,判断返回值的作用也就毫无意义,所以一旦返回(-1)就证明失败了,所以exec函数只有失败的返回值

六个函数的命名理解

exec:执行一个新的程序

p(path):表明在执行程序时,会在环境变量 PATH 所指定的路径中搜索要执行的程序。例如 execlp 和 execvp,这样就无需提供程序的完整路径。

#include <stdio.h>
#include <unistd.h>

int main() {
    // 使用 execlp 执行 ls 命令
    execlp("ls", "ls", "-l", NULL);
    perror("execlp failed");
    return 1;
}

l(list):表示参数以列表形式传递。例如 execl 和 execlp,调用这些函数时,需要依次列出新程序所需的参数,最后一个参数必须是 NULL。

#include <stdio.h>
#include <unistd.h>

int main() {
    // 使用 execl 执行 ls 命令
    execl("/bin/ls", "ls", "-l", NULL);
    perror("execl failed");
    return 1;
}

 

e(environ):表示可以传递自定义的环境变量给新程序。例如 execle 和 execve,需要额外提供一个包含环境变量的字符指针数组。(可以使用getenv()合并环境变量)

#include <stdio.h>
#include <unistd.h>

int main() {
    char *envp[] = {"MY_VAR=value", NULL};
    char *args[] = {"env", NULL};
    // 使用 execve 执行 env 命令并传递自定义环境变量
    execve("/usr/bin/env", args, envp);
    perror("execve failed");
    return 1;
}

v(vector):意味着参数以数组形式传递。像 execv 和 execvp,你需要创建一个包含所有参数的字符指针数组,数组的最后一个元素必须为 NULL。

#include <unistd.h>

int main() {
    char *args[] = {"ls", "-l", NULL};
    // 使用 execv 执行 ls 命令
    execv("/bin/ls", args);
    perror("execv failed");
    return 1;
}

 

其实只有execve是系统调用,其他都是库函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值