进程创建,退出以及各种进程

进程创建与管理详解

一,进程创建

创建进程有两种方式:1)操作系统创建   2)⽗进程创建。

进程的创建可以通过system函数、exec系列函数(execl、execlp等)、fork函数实现。

1)system函数

函数原型为:int system(const char*filename)
使⽤时需要包含头⽂件: stdlib.h
主要用于执行shell命令。

功能为:建⽴独⽴进程,拥有独⽴的代码空间,内存空间,等待新的进程执⾏完
毕,system才返回(阻塞),system函数其实内部会调⽤fork函数来产⽣⼦进程。


2)复制进程fork函数

函数原型为: pid_t fork(void)
 
功能:fork 函数会新⽣成⼀个进程,调⽤ fork 函数的进程为⽗进程,新⽣成的进
程为⼦进程。⼦进程复制了⽗进程打开的⽂件描述符。

函数返回值:函数返回类型 pid_t 实质是 int 类型。
在⽗进程中返回⼦进程的 pid,在⼦进程中返回 0,失败返回-1。
所属头⽂件:unistd.h
fork函数被⽗进程调⽤⼀次,但是却返回两次;⼀次是返回到⽗进程(⼦进程
id),⼀次是返回到新创建的⼦进程(是0)。
被fork的⼦进程会和和它⽗进程⼀样,继续执⾏当前的程序。

如图所示:pid = fork();子进程被创建,和它的父进程一样,继续执行剩下的程序。

1.父进程的数据空间,堆栈空间都会复制给子进程,而不是共享这些内存
2.子进程会继承父进程的一些属性,比如共享的存储段,当前工作目录,上下文环境等;子进程也有不同父进程的一些属性:没有继承比如进程pid;fork的返回值不同。

●fork()函数出错可能有两种原因:
  1、当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
  2、系统内存不⾜,这时errno的值被设置为ENOMEM

3)孤儿进程

如果⼀个⼦进程的⽗进程先与⼦进程结束,⼦进程就变成孤⼉进程;孤⼉进程将被
init进程(进程号为1)所收养,并由init进程对它们完成状态收集⼯作(包括资源回
收)。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
 pid_t pid=fork();
 if(pid==0)
 {
while(1){
 printf("⼦进程的id=%d⽗进程id=%d\n",getpid(),getppid());
sleep(3);} //延时⽬的是为了产⽣孤⼉进程
 }
 else if(pid>0)
 { 
 printf("⽗进程=id=%d\n",getpid());
 exit(0);
 }
 else
 {
 printf("创建进程错误!"); 
 }
 printf("end=%d\n",getpid());
 return 0;
}

4)进程等待

创建⼀个⼦进程后,⽗进程与⼦进程会争夺cpu,抢到者先执⾏,另⼀个挂起等待,
如果想要⽗进程等待⼦进程执⾏完毕以后再继续执⾏,可以在fork之后,调⽤wait
或waitpid。同时⽗进程通过上述函数还可以取得⼦进程的终⽌状态。

等待进程结束的函数主要有:
1,pid_t wait(int *ststus);
功能:wait函数会在⽗进程中阻塞,等待⼦进程结束,如果⼦进程结束,则返回子进程的PID。如果没有子进程则⽴刻返回-1。
注意:如果要获取wait的状态码,需要⽤该宏包裹
WEXITSTATUS(status)才可以拿到。

2,pid_t waitpid(pid_t pid, int *status, int options);
功能:waitpid函数等待⼦进程结束(options设置为WNOHANG时为⾮阻塞,设置为0 表示阻塞),如果⼦进程结束,则返回⼦进程的PID。如果没有⼦进程则⽴刻返 回-1,如果是⾮阻塞的并且⼦进程还没有结束,则返回0。

两者的区别:
wait函数作⽤就是⽗进程调⽤,将⽗进程挂起,直到所等待的进程结束,然后在执 ⾏⽗进程。⽽如果有多个⼦进程的话,只要第⼀个⼦进程结束,wait就会接受到信 号,并结束等待。⽽waitpid就会指定⼀个进程号,⽗进程将会等待pid这个⼦进程 结束才会执⾏。

5)僵尸进程及处理办法

(1)僵死进程概念:
⼦进程先于⽗进程结束,⽗进程没有调⽤ wait 获取⼦进程退出码。

(2) 如何处理僵死进程:⽗进程通过调⽤ wait()完成。

虽然⼦进程的主体结束,但是进程的PCB依旧存在 ,如果进程不调⽤wait / waitpid, 那么保留的那段信息就不会释放,其进程号就会⼀直被占⽤,但是系统 所能使⽤的进程号是有限的,如果⼤量的产⽣僵死进程,将因为没有可⽤的进程号 ⽽导致系统不能产⽣新的进程. 此即为僵⼫进程的危害,应当避免。

6)进程退出

进程退出表示进程即将结束运⾏。进程的退出分为两种:正常退出异常退出

(1)正常结束进程

  1、主函数结束,进程终⽌(即执⾏return);
  2、调⽤exit函数进程终⽌。
     函数原型:int exit(int status)
     参数:进程结束后要返回给⽗进程的值。该函数可以在程序的任何位置结束进程。
  3、main函数调⽤_exit函数
注意:exit在头⽂⽂件stdlib.h中声明,⽽ _exit在unistd.h中声明。

(2)进程的异常结束

1调⽤abort异常终⽌进程。
      函数原型:void abort();
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
 {
 pid_t pid = 0;
 pid = fork();
 int status;
 if(pid == 0 )
 {
 absort();
 }
 if(pid > 0 )
 {
 wait(&status);
 printf("CODE= %d \n",WEXITSTATUS(status) );
 }
 return 0;
 }

7)vfork函数

创建⼀个新进程的⽅法只能由某个已存在的进程调⽤fork()或vfork()进⾏创建,两者的主要区别是:
1)fork: ⼦进程拷⻉⽗进程的数据段和堆栈段,⽽vfork:⽗⼦进程共享数据段
2)vfork:保证⼦进程先运⾏,在它调⽤exec或exit之后⽗进程才能调度运⾏。

vfork函数⽣成的子进程与父进程共享数据。并优先执行

8)多进程下对⽂件描述符的影响

当进程执⾏fork时,⼦进程会复制⽗进程的所有信息,并共享⼀些数据,包括文件
描述符。⼦进程会复制⽗进程的所有FD的表记录,包括FD标志信息、⽂件指针。
这也就意味着,由⽗进程通过open创建出来的⽂件表,对于⼦进程来说是可⻅
的。⼦进程、⽗进程共享同⼀⽂件表对象!
子进程可以正常使用父进程的文件描述符
总结:
1.⽗⼦进程共享⽂件描述符的条件:在fork之前打开⽂件
2.对于两个完全不相关的进程,⽂件描述符不能共享。
3.⽗⼦进程⽂件描述符是共享的,但是关闭的时候可以分别关闭,也可以同时在 公有代码中关闭。

9)exec函数(夺舍函数)

⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分
⽀),该⼦进程⼏乎是⽗进程的副本,⽽有时我们希望⼦进程去执⾏另外的程序,
exec函数族就提供了⼀个在进程中启动另⼀个程序执⾏的⽅法。
与fork相⽐,fork创建⼀个新的进程,产⽣⼀个新的PID,exec启动⼀个新的程序
替换当前的进程,但PID不变。
所属头⽂件:#include <unistd.h>
 
函数作⽤:根据指定的⽂件名或路径找到可执⾏⽂件,并⽤它来取代调⽤进程的内
容。
 
函数参数
exec系列函数中的参数可以分成3个部分:  
   执行文件部分: path⽤来指定⼀个是⽂件的绝对路径
   命令参数部分: arg:就是给要执⾏⽂件传⼊的参数 部分
   环境变量
      
函数返回值:
成功 -> 函数不会返回,出错 -> 返回-1,失败原因记录在error中。

使用演示:
创建被调用的程序 (add.c):

#include <stdio.h>
#include <stdlib.h> // 为了使用 atoi 函数
#include <unistd.h>

int main(int argc, const char *argv[]) {
    // argc 是命令行参数的数量
    // argv 是一个指向参数字符串的指针数组

    // 检查传入的参数数量是否正确(程序名 + 两个数字)
    if (argc < 3) {
        printf("用法: %s <数字1> <数字2>\n", argv[0]);
        return 1; // 参数不足,返回错误
    }

    // 使用 atoi 将字符串参数转换为整数
    int num1 = atoi(argv[1]);
    int num2 = atoi(argv[2]);

    int sum = num1 + num2;

    // 打印传递进来的参数和计算结果
    printf("我是 'add' 程序,我收到的参数是 %s 和 %s\n", argv[1], argv[2]);
    printf("计算结果: %d + %d = %d\n", num1, num2, sum);

    return 0;
}

创建调用程序 (main.c):

#include <stdio.h>
#include <unistd.h>  // 为了使用 fork 和 execl
#include <sys/wait.h> // 为了使用 wait
#include <stdlib.h>   // 为了使用 perror

int main() {
    pid_t pid;

    printf("主程序(main)开始执行...\n");

    // 创建一个子进程
    pid = fork();

    // fork 调用失败
    if (pid < 0) {
        perror("fork 失败");
        return 1;
    } 
    // 这是子进程执行的代码块
    else if (pid == 0) {
        printf("这里是子进程 (PID: %d),我将要变身去执行 'add' 程序...\n", getpid());

        // 使用 execl 来执行新的程序
        // 参数说明:
        // 1. "/path/to/add": 要执行的程序路径 (这里假设在同一目录,用 "./add")
        // 2. "add_program": 传给新程序的 argv[0] (通常是程序名)
        // 3. "25": 传给新程序的 argv[1]
        // 4. "8": 传给新程序的 argv[2]
        // 5. NULL: 参数列表结束的标志
        execl("./add", "add_program", "25", "8", NULL);

        // 如果 execl 调用成功,它永远不会返回。
        // 所以,如果下面的代码被执行了,就说明 execl 调用出错了。
        perror("execl 失败");
        exit(1); // 必须退出,否则会继续执行父进程的代码
    }
    // 这是父进程执行的代码块
    else {
        printf("这里是父进程 (PID: %d),我正在等待子进程执行完毕...\n", getpid());

        // 等待子进程结束
        wait(NULL); 

        printf("父进程:子进程已经执行完毕,主程序(main)结束。\n");
    }

    // 注意:子进程成功执行 execl 后,它自己的代码(包括下面的 return)就被替换掉了,
    // 所以子进程永远不会执行到这里。
    return 0;
}

编译并运行这两个文件,先编译add.c:gcc add.c -o add     编译main.c:gcc main.c -o main  
运行主程序:./main
会看到类似输出效果:

注意:如果⽤到了exec函数族,⼀定记得要加错误判断语句,避免因找不到⽂件或
其他原因造成的问题。
应⽤场景:⽐如⼀个进程想执⾏另⼀个程序,那么它就可以调⽤fork()函数新建⼀
个进程,然后调⽤exec函数族中的任意⼀个函数,这样看起来就像通过执⾏应⽤程
序⽽产⽣了⼀个新进程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值