进程线程 笔记01

进程

一、什么是进程

  1. 进程是一个程序的一次执行的过程 ./a.out

  2. 进程是一个独立的可调度的任务

  3. 进程是程序执行和资源管理的最小单位(进程是资源分配的最小单位)

  4. 进程和程序的区别

    • 程序:静态的, 保存在磁盘上的有序指令的集合,无执行的概念
    • 进程:动态的, 一个程序的执行过程,进程有生命周期(创建,运行,消亡),进程(代码段 数据段 堆栈)
  5. 进程的特性

    • 并发性 //允许统一时间同时运行多个进程
    • 动态性 //进程有生命周期,创建 运行 消亡
    • 交互性 //scanf输入 与用户交互
    • 独立性 //每个进程都有自己独立地址空间
  6. 进程分类

    • 交互式进程、批处理进程、守护进程

二、查看所有进程命令

ps aux 显示所有进程的信息
top动态显示进程信息 ,按q退出

三、进程关系树(init、PID)

pstree 列出进程关系树 最先启动进程 init(PID 1号)

四、在程序中如何获取当前进程的pid:getpid()和getppid()

每个进程间的运行,都有一个自己的ID,叫PID

//头文件
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void); // 获取当前进程的PID
pid_t getppid(void); // 获取当前进程的父进程的PID 中间的p 是parent的缩写(父母)
typedef int pid_t; // 类型重定义

代码演示

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

int main(int argc, const char *argv[])
{
    pid_t pid = getpid();
    pid_t ppid = getppid();
    printf("当前进程的PID是%d\n",pid);
    printf("当前进程的父进程的PID是%d\n",ppid);
    return 0;
}

五、进程说明

  1. pid号的顺序:顺序增加
  2. pid的回收:每个进程运行结束后,系统会回收进程ID(PID),但不会立即使用,进程的PID号不是无限的
  3. 进程与终端:所有在终端上运行的进程其父进程都为终端,终端结束,其终端上的进程也结束

六、进程状态(就绪态 运行态 阻塞态 见图)

就绪态 运行态 阻塞态 三态之间的转换

可以造成阻塞的函数 scanf getchar gets sleep(10)

进程状态图

七、如何创建一个子进程

1.fork()函数介绍

typedef int pid_t; // 类型重定义
#include <unistd.h> 头文件
pid_t fork(void); 创建一个子进程

功能:

用于创建一个子进程,当执行完fork 就会出现一个新进程(新进程称为子进程,原来的进程称为父进程)fork 执行完 子进程会复制 父进程几乎所有东西,并且从fork后面语句开始执行,调用一次,返回两次

代码演示
#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    fork();//执行完fork函数,就会产生一个子进程,子进程从fork函数的下一行代码开始执行
    printf("hello world!\n");
    return 0;
}

查看打印结果

打印输出结果:
linux@ubuntu:~$ ./a.out
hello world!
hello world!
linux@ubuntu:~$

两遍hello world 一个是父进程打印的,一个是子进程打印的
一个是父进程 一个是子进程
最先走的是父进程,执行完fork之后,才有了子进程
父子进程谁先执行,不确定,是随机的,由cpu决定

可以使用getppid和getpid来获取父子进程的pid值

为什么子进程里面,父进程的打印是1 ?
执行完fork之后,才有了子进程,父子进程谁先执行,不确定,由cpu决定,随机之后,cpu先处理的是 父进程,父进程先结束了,父进程结束后,子进程由init进程接管(1 福利院)
那么如何让子进程先结束,父进程后结束呢?
可以在父进程中加个延时

2.如何判断,父进程和子进程

通过fork函数的返回值可以区分当前的进程是父进程还是子进程
pid_t pid = fork();
pid == 0 说明当前的进程是子进程
pid > 0 说明当前的进程是父进程, 并且返回值pid 是子进程的PID
pid == -1 说明创建一个子进程失败

代码演示
#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    pid_t pid = fork();//执行完fork函数,就会产生一个子进程,子进程从fork函数的下一行代码开始执行

    if(pid == -1)
    {
        perror("创建子进程失败");
        return -1;
    }
    else if(pid == 0)//说明当前进程是子进程,子进程会进入该条件分支
    {
        printf("子进程!!\n");
        printf("子进程的PID:%d 父进程的PID:%d\n",getpid(),getppid());
        printf("子进程中fork函数的返回值是:%d\n",pid);
    }
    else if(pid > 0)//说明当前进程是父进程,父进程会进入该条件分支
    {
        sleep(1);//延时1s,cpu不可能等待1s,cpu会立刻取处理其他的任务,为了让子进程先结束,父进程后结束
        printf("父进程!!\n");
        printf("父进程的PID:%d 父进程的父进程的PID是%d\n",getpid(),getppid());
        printf("父进程中fork函数的返回值是:%d\n",pid);
    }

    printf("hello world!!\n");
    return 0;
}

查看输出结果

linux@ubuntu:~$ ./a.out
子进程!!
子进程的PID:15211 父进程的PID:15210
子进程中fork函数的返回值是:0
hello world!!
父进程!!
父进程的PID:15210 父进程的父进程的PID是15102
父进程中fork函数的返回值是:15211
hello world!!
linux@ubuntu:~$

八、for 循环创建多进程实例

#include <stdio.h>
    int main()
    {
        int i;
        for(i = 0; i < 3; i++)
        {
            fork();
        }
        printf("11111111111\n");
    }

思考:11111111111打印了多少次?

查看答案

打印8次

九、_exit和exit的区别

exit()函数——在结束程序前,会刷新缓存区

  1. 功能:
    用于结束进程,当程序执行到exit函数时,程序就结束 exit(0);
  2. 头文件与函数原型
    #include <stdlib.h> void exit(int status);
  3. 参数:
    status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束,其它值表示出现了错误,进程非正常结束

_exit()——在结束程序前,不会刷新缓存区

  1. 功能:
    用于结束进程,当程序执行到_exit函数时,_exit(0);
  2. 头文件与函数原型
    #include <stdlib.h> void _exit(int status); status结束状态,0,表示正常结束
#include
#include 
#include 
int main(int argc, const char *argv[])
{
printf("hello world");//注意此处没有\n
_exit(0);//hello world不会被打印,因为_exit函数后,直接结束程序,不刷新缓存区
printf("11111111111111\n");//1111111不会打印,因为执行完_exit程序结束
return 0;
}

十、资源回收

父进程不应该比子进程先结束,父进程要负责回收子进程的资源,

孤儿进程

父进程先结束,子进程未结束,此时子进程会由1号 init进程接管(福利院)此时的子进程称为孤儿进程

僵尸进程

子进程先结束,父进程未结束,但是父进程又没有调用wait族函数来回收子进程的资源,此时的子进程称为 僵尸进程

linux查看进程信息
ps aux | grep a.out
发现子进程的状态是 Z+ ,僵尸进程

如何回收子进程资源

调用wait族函数,回收子进程的资源

wait()函数
  1. 功能:
    wait(当父进程执行此函数时,父进程阻塞等待子进程结束)父进程执行到wait函数就会阻塞,等待子进程的结束,一旦有一个子进程结束,wait将结束阻塞当子进程结束时,父进程会通过wait函数回收子进程的资源

  2. 头文件及函数原型

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
  3. 函数参数
    status是一个整型指针,指向的对象用来保存子进程退出时的状态。 status若为空,表示忽略子进程退出时的状态 status若不为空,表示保存子进程退出时的状态

  4. 函数返回值
    成功:pid_t //返回值是结束的那个子进程的id
    失败:-1

//子进程延时仍无效
//对阻塞功能和返回值的验证(延时无效)
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
    pid_t pid = fork();//执行完fork函数,就会产生一个子进程,子进程从fork函数的下一行代码开始执行
    if(pid == -1)
    {
        perror("创建子进程失败");
        return -1;
    }
    else if(pid == 0)//说明当前进程是子进程,子进程会进入该条件分支
    {
        sleep(3);//在子进程加上延时,本来想让父进程先结束,但是目的仍未达到
        printf("子进程PID是:%d\n",getpid());
    }
    else if(pid > 0)//说明当前进程是父进程,父进程会进入该条件分支
    {
        int ret  = wait(NULL);//阻塞等待子进程的结束,直到子进程结束,wait函数才会解除阻塞,回收资源,继续向下执行
        printf("ret is %d\n",ret);//ret的值,就是结束的那个子进程的PID
        printf("父进程PID是:%d\n",getpid());
    }

    return 0;
}
waitpid()函数
  1. 功能:指定等待子进程的结束
  2. 头文件及函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);     //等待指定的子进程结束
int options //你可以选择让 waitpid()函数阻塞等待 也可以选择 非阻塞等待
  1. 参数说明
    pid_t pid pid > 0 回收进程的ID
    int *status 同wait
    options:
    WNOHANG 表示不阻塞,waitpid不阻塞而立即返回,此时值为0,结束后返回值为结束子进程pid
    0 阻塞
  2. 返回值:
    大于0: 已经结束运行的子进程进程号
    等于0: 使用WNOHANG且没有子进程退出
    等于-1:出错
  3. 应用举例
    waitpid(31111, NULL, 0); //指定阻塞等待PID 31111 子进程结束
    wait(NULL);

waitpid()阻塞等待子进程结束代码举例

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

int main(int argc, const char *argv[])
{
    pid_t pid = fork();//执行完fork函数,就会产生一个子进程,子进程从fork函数的下一行代码开始执行

    if(pid == -1)
    {
        perror("创建子进程失败");
        return -1;
    }
    else if(pid == 0)//说明当前进程是子进程,子进程会进入该条件分支
    {
        sleep(3);//在子进程加上延时,本来想让父进程先结束
        printf("子进程PID是:%d\n",getpid());
    }
    else if(pid > 0)//说明当前进程是父进程,父进程会进入该条件分支
    {
        //父进程里fork函数的返回值>0,值就是子进程PID
        int ret  = waitpid(pid,NULL,0);//第三个参数0,表示阻塞,阻塞等待子进程的结束,直到子进程结束,waitpid函数才会解除阻塞,继续向下执行
        printf("ret is %d\n",ret);//ret的值,就是结束的那个子进程的PID
        printf("父进程PID是:%d\n",getpid());
    }

    return 0;
}

waitpid()非阻塞等待子进程结束代码举例

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

int main(int argc, const char *argv[])
{
    pid_t pid = fork();
    if(pid == 0) //子进程活了5s
    { 
        sleep(5);
        exit(0);
    }
    else if(pid  > 0)
    {
        int ret;
        do
        {
            //第三个参数是WNOHANG说明waitpid函数,是非阻塞的
            //调用waitpid函数后,如果 pid这个子进程没有结束,那么waitpid函数的返回值是0

            //当子进程结束后,再调用waitpid()函数时候,返回值 就是结束的哪个子进程的PID

            ret = waitpid(pid,NULL,WNOHANG);
            if(ret == 0) //如果返回值是0,说明子进程活着
            {
                printf("子进程活着!!\n");
            }
            else if(ret == pid)//说明子进程已经结束了
            {
                printf("子进程死了!1\n");
            }
            sleep(1);//延时1s,循环,每隔1s,检查一次子进程是否结束
        }while(ret != pid); // ret == pid的时候,循环结束
    }
    return 0;
}

打印输出结果:
子进程活着!!!
子进程活着!!!
子进程活着!!!
子进程活着!!!
子进程活着!!!//打印5次,是因为子进程活了5s,每隔1s检查一次子进程是否结束
子进程跪了!!!

十一、exec函数族

调用exec族函数中的一个,可以实现去执行一个新的程序

功能

用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。
execl execv execle execve execlp ececvp

头文件及函数原型

#include <unistd.h>

    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 execvpe(const char *file, char *const argv[], char *const envp[]);

l //list 缩写 希望接收以逗号分隔的参数列表,列表以NULL作为结尾标志
p //PATH 系统会按照PATH环境变量进行路径进行查找命令          
e //enviroment 缩写 指定当前进程所使用的环境变量
v //vertor 缩写 参数传递为以NULL结尾的字符指针数组

六个函数返回:若出错则为- 1,若成功则不返回

参数说明

char * file 执行程序的名称(可以包含路径"/home/linux/hello")
char *arg, ... 具体执行这个程序,需要在命令行传递的参数

返回值

出错返回-1
成功无返回值

代码演示1

//gcc -o hello hello.c 
////hello.c//////////////
#include <stdio.h>

int main(int argc, const char *argv[])
{
    if(argc != 3)
    {
        printf("忘记传递参数了!!\n");
        return -1;
    }
    printf("新的程序hello被执行了!!\n");
    printf("argv[1] is %s\n",argv[1]);
    printf("argv[2] is %s\n",argv[2]);
    return 0;
}

代码演示2

//gcc -o hello hello.c 
////////test.c/////////////////////

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

int main(int argc, const char *argv[])
{
    //第一参数,是执行新程序的名称包含路径
    //第二参数,开始是这个程序 在命令行的执行过程
    //注意,最后的参数一定是NULL,表示参数结束
    execlp("/home/linux/hello","./hello","aaaa","bbbb",NULL);
    printf("hello world!!\n");//会被打印吗?不会被打印
    //没有打印,因为执行完execlp后,当前进程的空间的所有内容由
    //新的程序 hello,替换,重新开始从hello程序main函数开始执行
    return 0;
}


execlp 经常与多进程组合使用,用一个子进程单独执行execlp程序

End

本文标题:Linux-C进程线程 笔记01

本文链接:https://www.chisato.cn/index.php/archives/40/

除非另有说明,本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

声明:转载请注明文章来源。

最后修改:2021 年 11 月 21 日 05 : 39 PM
如果觉得我的文章对你有用,请随意赞赏