1.进程的基本概念

        进程:是程序动态执行的过程,包括创建、调度、消亡。

        程序:存放在外存中的一段数据的集合。

2.进程的创建

        2.1 进程空间分布

        进程空间由用户空间和内核空间(不允许用户访问)构成,每个进程运行起来后,操作系统开辟 0 - 4G虚拟内存空间。用户空间就是文本段(文本区) + 数据段(数据区) + 系统数据段(堆区、栈区)。

        2.2 虚拟地址和物理地址

        1. 虚拟地址: 所有能够被用户看到的地址均为虚拟地址,表示用户可以寻址的范围

        2. 物理地址: 内存存放数据对应的实际硬件物理地址

        3. MMU: 实际地址和物理地址之间的映射由内存映射单元(MMU)完成

         

        2.3 多进程存储

                各个进程虚拟空间一致,物理空间不同

        2.4多进程的调度

                常见的进程调度算法有1)先来先执行,后来后执行。2)高优先级调度算法。3)时间片轮转调度算法,多进程调度本质是宏观并行,微观串行。宏观:一个CPU同时执行多个进程任务。微观:一个CPU在多个任务中高速切换保障多任务执行。(好比一个厨子同时做10道菜,宏观来看他是在同时进行,但微观层面他只能在同一时刻做一道菜)

3.进程的相关命令

3.1top

示例:top
功能:根据CPU占用率和内存占用率查看当前所有进程的信息
 
PID:唯一区分进程的ID号
USER:创建者
PR  NI:优先级
VIRT    
RES   
SHR:资源占用
S:状态
%CPU:CPU占用率
%MEM:内存占用率
TIME+:运行的时长
COMMAND:进程命令
q退出

3.2ps -ef

示例:ps -ef
功能:查看该时刻的所有进程信息
UID:创建进程的用户         
PID:进程的ID号   
PPID:父进程的ID号  
C 
STIME 
TTY:进程依赖的终端        
TIME 
CMD
 ps -ef | grep 进程名
查找与进程名对应的进程信息

3.3ps -aux

示例:ps -aux
功能:查看该时刻的所有进程信息
USER
 PID 
%CPU 
%MEM    
VSZ   
RSS 
TTY      
STAT:进程的状态
START   TIME 
COMMAND

3.4后台执行进程任务

示例:./a.out &
功能:后台执行a.out进程

3.5jobs

示例:jobs
功能:查看终端下所有的后台进程任务

3.6fg

示例:fg 编号(通过jobs查看)
功能:后台执行的任务放到前台执行

3.7nice/renice

示例:nice -n 优先级编号 进程名
     renice -n 优先级编号 进程PID
功能:改变进程的优先级
优先级范围:-20 - 20
数字越小优先级越高

3.8kill/killall

示例:kill -编号 进程PID
     killall -编号 进程名
功能:杀死进程任务
kill -9 PID

4.进程的状态

进程状态标识含义
就绪态/运行态R进程位于CPU任务调度的队列中
可唤醒等待态S等待某个资源的到来,资源到来后被唤醒加入调度队列 等待过程中可以被打断
不可唤醒等待态D等待某个资源的到来,资源到来后被唤醒加入调度队列 等待过程中不可以被打断
停止态T用户人为让进程任务暂停等待
僵尸态Z进程代码运行结束,空间尚未回收

5.进程相关的函数接口

5.1fork创建一个进程

原型:pid_t fork(void);
功能:
创建一个新的进程,新的进程称为子进程,调用fork的进程称为父进程
参数:
缺省
返回值:
父进程中返回子进程的PID
子进程中返回0 
出错返回-1 

fork()就像细胞分裂:
1)调用 fork()时,当前进程(父进程)会复制一个几乎完全一样的子进程。子进程和父进程的代码、变量、打开的文件等几乎完全相同(但独立运行)

2)fork()的返回值不同:
父进程:返回子进程的PID(一个正数)。
子进程:返回 0(表示自己是子进程)。
出错:返回 -1(比如内存不足,无法创建子进程)。


3)fork()之后,父子进程同时运行,谁先执行取决于操作系统的调度。

一个父进程创建一个子进程:

#include "head.h"
int main(void)
{
    pid_t pid;  // 定义一个变量 pid,用于存储 fork() 的返回值
    pid = fork();  // 调用 fork() 创建子进程
    // fork() 会复制当前进程,生成一个几乎完全相同的子进程
    // 注意:fork() 调用一次,但返回两次(父进程和子进程各返回一次)
    if (-1 == pid)  // 如果 fork() 失败(比如系统资源不足)
    {
        perror("fail to fork");  // 打印错误信息
        return -1;  // 退出程序
    }

    if (0 == pid)  // 如果 fork() 返回 0,说明当前是子进程
    {
        // 子进程会执行这里的代码
        printf("我是子进程, PID:%d PPID:%d\n", getpid(), getppid());
        // getpid() 获取当前进程ID(子进程自己的PID)
        // getppid() 获取父进程ID
    }
    else if (pid > 0)  // 如果 fork() 返回一个正数,说明当前是父进程
    {
        // 父进程会执行这里的代码
        // pid 的值就是子进程的PID
        printf("我是父进程, PID:%d childpid:%d\n", getpid(), pid);
    }

    // 注意:fork() 之后,父子进程都会继续执行后面的代码!
    printf("hello world\n");  // 父子进程都会打印这句话

    while (1)  // 让父子进程都进入死循环(防止程序退出,方便观察)
    {
        // 空循环,不做任何事情
    }

    return 0;  // 由于 while(1),实际不会执行到这里
}

一个父进程创建两个子进程:

#include "head.h"
int main (void)//一父俩子
{
    pid_t pid1;
    pid_t pid2;
    pid1 = fork();
    if(-1 == pid1)
    {
        perror("fail to fork");
        return -1;
    }
    if(0 == pid1)
    {
            printf("我是子进程1, PID:%d PPID:%d\n", getpid(), getppid());
    }
    else if (pid1 > 0)  
    {
        
        pid2 = fork();
        if(-1 == pid2)
        {
            perror("fail to fork");
            return -1;
        } 
        if(0 == pid2)
        {
            printf("我是子进程2, PID:%d PPID:%d\n", getpid(), getppid());
        }
        else if (pid2 > 0)
        {
            printf("我是父进程, 我的PID:%d 我儿子childpid1:%d childpid2:%d\n", getpid(),pid1, pid2);   
        }
    }
    while (1)
    {
        /* code */
    }
    return 0;
}

需要注意的是,子进程会把父进程缓存区也拷贝过去,因此父子进程都会打印hello world。

#include "head.h"
int main(void)
{
    pid_t pid;  
    printf("hello world");
    pid = fork();
    if (-1 == pid)  
    {
        perror("fail to fork");
        return -1; 
    }

    if (0 == pid)  
    {
        printf("我是子进程\n");
        
    }
    if (pid > 0)
    {
        printf("我是父进程\n");
    }
    return 0; 
}

知识扩展之写时拷贝

        写时拷贝(Copy-0n-Write,COW)是一种高效的资源管理技术,核心思想是"只有在真正需要修改时才进行拷贝"。

5.2getpid和getppid

具体功能已在fork展示

原型:pid_t getpid(void);
功能:获得调用函数进程的PID号
原型:pid_t getppid(void);
功能:获得调用函数进程的父进程的PID号

5.3exit与_exit

原型:void exit(int status);
功能:
    结束进程任务并返回进程结束状态
参数:
    status:进程结束状态的值
    
原型:void _exit(int status);
功能:
    结束进程任务并返回进程结束状态
参数:
    status:进程结束状态的值
#include "../head.h"

int main(void)
{
    pid_t pid;

    pid = fork();
    if (-1 == pid)
    {
        perror("fail to fork");
        return -1;
    }
    if (0 == pid)
    {
        while (1)
        {
            printf("PID:%d PPID:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if (pid > 0)
    {
        printf("父进程即将退出!");
        _exit(0);
    }

    return 0;
}

        需要注意:在主函数中调用exit和return功能保持一致, return在函数内部将结束该函数 ,exit在函数内部会将进程结束 ,exit会在结束前刷新缓存区 _exit不会刷新缓存区。

5.4wait

原型:pid_t wait(int *wstatus);
功能:
    回收子进程空间
参数:
    wstatus:存放子进程结束状态空间的首地址
返回值:
    成功返回回收到的子进程的PID
    失败返回-1 
    
WIFEXITED(wstatus):测试进程是否正常结束
WEXITSTATUS(wstatus):获得进程退出的值
WIFSIGNALED(wstatus):测试进程是否被杀死
WTERMSIG(wstatus):获得杀死该进程的信号编号

        wait具有阻塞等待功能,等到有子进程结束才会回收子进程继续向下执行 wait可以实现父子进程任务的同步。

#include "../head.h"

int main(void)
{
    pid_t pid;
    pid_t ret;

    pid = fork();
    if (-1 == pid)
    {
        perror("fail to fork");
        return -1;
    }
    if (0 == pid)
    {
        sleep(5);
        printf("子进程即将退出!\n");
        exit(0);
    }
    else if (pid > 0)
    {
        ret = wait(NULL);
        printf("回收到子进程(PID:%d)空间\n", ret);
        while (1)
        {
            
        }
    }

    return 0;
}

5.5waitpid

原型:pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:
回收指定的子进程空间
参数:
pid:要回收的进程的PID(-1表示回收任意子进程)
wstatus:存放子进程结束状态空间首地址
options:
 0       
阻塞回收
WNOHANG 非阻塞回收
返回值:
成功返回回收到的子进程PID
失败返回-1 
指定的子进程空间没结束,返回0
 waitpid(-1, NULL, 0) 等价于 wait(NULL)

6.进程消亡

6.1孤儿进程

        父进程先结束,子进程会成为孤儿进程,被init进程收养

用上面的一个父进程两个子进程举例:这里不加while循环,父进程先于子进程结束。子进程2会被init接管,其父进程pid会变成1:

#include "head.h"
int main (void)//一父俩子
{
    pid_t pid1;
    pid_t pid2;
    pid1 = fork();
    if(-1 == pid1)
    {
        perror("fail to fork");
        return -1;
    }
    if(0 == pid1)
    {
            printf("我是子进程1, PID:%d PPID:%d\n", getpid(), getppid());
    }
    else if (pid1 > 0)  
    {
        
        pid2 = fork();
        if(-1 == pid2)
        {
            perror("fail to fork");
            return -1;
        } 
        if(0 == pid2)
        {
            printf("我是子进程2, PID:%d PPID:%d\n", getpid(), getppid());
        }
        else if (pid2 > 0)
        {
            printf("我是父进程, 我的PID:%d 我儿子childpid1:%d childpid2:%d\n", getpid(),pid1, pid2);   
        }
    }
   // while (1)
    //{
        /* code */
    //}
    return 0;
}

加上while循环保证父进程不会结束,这样子进程2的父进程pid不会变为1

#include "head.h"
int main (void)//一父俩子
{
    pid_t pid1;
    pid_t pid2;
    pid1 = fork();
    if(-1 == pid1)
    {
        perror("fail to fork");
        return -1;
    }
    if(0 == pid1)
    {
            printf("我是子进程1, PID:%d PPID:%d\n", getpid(), getppid());
    }
    else if (pid1 > 0)  
    {
        
        pid2 = fork();
        if(-1 == pid2)
        {
            perror("fail to fork");
            return -1;
        } 
        if(0 == pid2)
        {
            printf("我是子进程2, PID:%d PPID:%d\n", getpid(), getppid());
        }
        else if (pid2 > 0)
        {
            printf("我是父进程, 我的PID:%d 我儿子childpid1:%d childpid2:%d\n", getpid(),pid1, pid2);   
        }
    }
    while (1)
    {
        /* code */
    }
    return 0;
}

6.2僵尸进程

        僵尸进程是每个进程结束必然会经历的阶段,子进程结束后,父进程没有回收子进程空间,导致进程执行结束,空间依然被占用的状 态,称为僵尸进程,子进程结束,父进程回收子进程空间,即可避免产生僵尸进程,或者父进程先结束,子进程会成为孤儿进程,孤儿进程被init进程收养,子进程再结束,init 进程回收进程空间。

7.exec函数族

7.1exec函数族

利用进程空间执行另一份代码,且不会返回当前代码,只有exec报错

extern char **environ;
 int execl(const char *path, const char *arg, .../* (char  *) NULL 
*/);
 int execlp(const char *file, const char *arg, .../* (char  *) NULL 
*/);
 int execle(const char *path, const char *arg, .../*, (char *) NULL, 
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:参数以列表的形式传递
p:在系统变量PATH对应的目录下查找文件
v:参数以指针数组的形式传递
e:执行新代码是更新环境变量

通过exec函数在main.c中调用hello

exec常搭配fork使用,fork负责创建新的子进程,exec负责让子进程执行自己的代码

演示在mysystem部分。

7.2主函数传参

用法在exec函数部分已经展示过了

 void main(void);
 int main(void);
 int main(int argc, const char *argv[]);
 int main(int argc, const char **argv);
 argc:传入参数的个数
argv:存放每一个传入参数指针的数组名
argv[0] = "./a.out"
 argv[1] = "hello" 
argv[2] = "world"
 argv[3] = "how"
 argv[...] = ...
 argv[argc] = ...

7.3system函数(执行命令并返回)

原型:int system(const char *command);
功能:
    运行command命令
参数:
    command:shell命令字符串首地址
返回值:
    成功返回0 
    失败返回-1 

用exec搭配fork写一个自己的system函数:

#include "head.h"

void mysystem(void)
{
    pid_t pid;

    pid = fork();
    if(-1 == pid)
    {
        perror("fail to fork");
        return;
    }
    if (0 == pid)
    {
        execlp( "ls","ls", "-l", NULL);
    }
    wait(NULL);

    return;
}

int main(void)
{
    printf("system上面\n");
    mysystem();
    printf("system下面\n");

    return 0;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐