Linux C语言 22-多进程

Linux C语言 22-进程

本节关键字:进程、exec函数族
相关C库函数:fork、getpid、getppid、getuid、geteuid、getgid、getegid、execl、execlp、execv、execvp、execle、execvpe

什么是进程?

  • 进程是程序的执行过程;
  • 进程是动态的,随着程序的使用被创建,随着程序的结束而消亡;
  • 进程是系统调度的独立任务;
  • 进程是程序执行的独立任务;
  • 进程是内存资源管理的最小任务。
    注意:一个程序可以只有一个进程,也可以有多个进程(程序由多个进程动态执行);每一个程序运行时,操作系统分配给进程的是虚拟内存,意味着每一个进程所使用的空间都是虚拟内存, 虚拟内存会被单元管理模块(MMU)映射到物理内存上,如何映射是操作系统关心的事情,程序开发者不用关心。

C程序的启动和终止:
Linux C语言 22-多进程_第1张图片

时间片

进程有多个,而CPU只有一个,假设该CPU是单核的,那么在某一时刻CPU只能处理一个进程,但是不能一直去处理这个进程,得多个进程之间轮流处理,给用户感觉这些进程在同时进行,而CPU处理一个进程的时间段即时间片。时间片是约定好CPU处理一个进程的时间段。

进程的类型

  • 交互进程:完成人机交互的进程,可以在前台运行,也可以在后台运行。
  • 批处理进程:与终端无关,被提交到一个作业队列中顺序执行。
  • 守护进程:和终端无关,一直到后台运行。

进程的状态

  • 运行态:正在占用CPU执行任务。
  • 等待态:又称阻塞态或睡眠态,缺少某些资源而让出CPU。
  • 就绪态:资源准备就绪,等待CPU调度。
    Linux C语言 22-多进程_第2张图片

进程的模式

  • 终端:内核发送的信号。
  • 系统调用:调用操作系统提供的访问硬件的一组接口。

特殊进程

特殊进程是指处于一种非常规状态的进程,在这里主要将其分为孤儿进程和僵尸进程。

孤儿进程

父进程比子进程先退出的进程称为孤儿进程,孤儿进程会被进程号为1的init进程收养。

僵尸进程

子进程比父进程先退出,但没有被父进程回收资源的进程称为僵尸进程,僵尸进程会造成空间浪费和资源泄漏等问题。

进程的状态标志

  • D 不可中断的静止
  • R 正在执行中
  • S 阻塞状态
  • T 暂停执行
  • Z 不存在但暂时无法消除
  • < 高优先级的进程
  • N 低优先级的进程
  • L 有内存分页分配并锁在内存中

父进程和子进程之间的关系

父进程和子进程之间对打开文件的共享
Linux C语言 22-多进程_第3张图片

除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:

  • 实际用户ID、实际组ID、有效用户ID、有效组ID
  • 附属组ID
  • 进程组ID
  • 会话ID
  • 控制终端
  • 设置用户ID标志和设置组ID标志
  • 当前工作目录
  • 根目录
  • 文件模式创建屏蔽字
  • 信号屏蔽和安排
  • 对任一打开文件描述符的执行时关闭(close-cm-exec)标志
  • 环境
  • 连接的共享存储段
  • 存储映像
  • 资源限制

父进程和子进程之间的区别具体如下:

  • fork的返回值不同。
  • 进程ID不同。
  • 这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程的ID,而父进程的父进程ID则不变。
  • 子进程的 tms_utime、tms_stime、tms_cutime 和 tms_ustime 的值设置为 0
  • 子进程不继承父进程设置的文件锁。
  • 子进程的未处理闹钟被清除。
  • 子进程的未处理信号集设置为空集。

进程相关库函数

#include 
// 通过复制调用进程来创建一个新进程,子进程返回0,父进程返回子进程ID,出错时父进程返回-1,并设置error为错误码
// 复制的子进程是从父进程fork()调用后面的语句开始执行的
// EAGIN 无法分配足够的内存来复制父级的页表并为子级分配任务结构
// ENOMEM 由于内存紧张,fork()无法分配必要的内核结构
// SENOSYS 此平台不支持fork()(例如,没有内存管理单元的硬件)
// ERESTARTNOINTR 系统调用被信号中断,将重新启动。(这只能在跟踪过程中看到
pid_t fork(void);

// 进程标识
// 获取当前进程的ID
pid_t getpid(void);
// 获取当前进程的父进程的ID
pid_t getppid(void);
// 获取当前进程实际用户ID
uid_t getuid(void);
// 获取当前进程有效用户ID
uid_t geteuid(void);
// 获取当前进程使用用户组ID
gid_t getgid();
// 获取当前进程有效用户组ID
gid_t getegid();

// 进程退出 将status传递给父进程
#include 
void exit(int status);

// 进程回收 
#include 
// 阻塞等待进程号为*stat_loc的进程退出
pid_t wait(int *stat_loc);

// 等待子进程退出
// 如果pid == (pid_t)-1,options为0,则waitpid函数等效于wait函数
// 如果pid == (pid_t)-1,则会请求任何子进程的状态
// 如果pid > 0,则指定请求状态的单个子进程的进程ID
// 如果pid == 0,则会为进程组ID等于调用进程的进程组ID的任何子进程请求状态
// 如果pid < (pid_t)-1,则会为进程组ID等于pid绝对值的任何子进程请求状态
// WCONTINUED 报告pid指定的任何连续子进程的状态,该进程的状态自作业控制停止后一直没有报告
// WNOHANG 如果pid指定的某个子进程的状态不立即可用,则waitpid函数不应暂停调用线程的执行
// WUNTRACED pid指定的任何已停止的子进程的状态,以及自停止以来尚未报告其状态的子进程,也应报告给请求进程
// 如果调用进程设置了SA_NOCLDWAIT或SIGCHLD设置为SIG_IGN,并且该进程对于转换为僵尸进程的子进程没有未经访问的权限,则调用线程应阻止,直到包含调用线程的进程的所有子进程终止,wait()和waitpid()将失败并将errno设置为[ECHILD]
pid_t waitpid(pid_t pid, int *stat_loc, int options);

进程相关库函数使用示例

#include 
#include 

void test01(void)
{
    printf("======= main process begin =======\n");
    
    int res = 10;
    pid_t pid;
    
    pid = fork();
    if (pid == -1)
    {
        perror("fork error");
        return;
    }
    if (pid == 0) // 子进程
    {
        printf("i am child: %d, my parent: %d\n", getpid(), getppid());
        printf("i am child: uid[%d] euid[%d] gid[%d] egid[%d]\n", 
            getuid(), geteuid(), getgid(), getegid());
        
        while (res <= 20)
        {
            sleep(2);
            
            res += 1;
            printf("child check res: %d\n", res);
        }
    }
    if (pid > 0) // 父进程
    {
        printf("i am main: %d, my child: %d, my parent: %d\n", getpid(), pid, getppid());
        printf("i am main: uid[%d] euid[%d] gid[%d] egid[%d]\n", 
            getuid(), geteuid(), getgid(), getegid());
        while (res >= 0)
        {
            sleep(1);
            
            res -= 2;
            printf("main check res: %d\n", res);
        }
        
        printf("i am main, i am waiting child\n");
        wait(&pid); // 防止僵尸进程的出现
        printf("i am main, i will exit\n");
    }
    printf("======= main process end =======\n");
}

void test02(void)
{
    printf("======= main process begin =======\n");
    pid_t pid;
    
    pid = fork();
    if (pid == -1)
    {
        perror("fork error");
        return;
    }
    if (pid == 0) // 子进程
    {
        printf("i am child: %d, my parent: %d\n", getpid(), getppid());
        
        while (1)
        {
            sleep(2);
        }
    }
    if (pid > 0) // 父进程
    {
        printf("i am main: %d, my child: %d, my parent: %d\n", getpid(), pid, getppid());
        
        while (1)
        {
            sleep(1);
        }
        
        printf("i am main, i am waiting child\n");
        wait(&pid); // 防止僵尸进程的出现
        printf("i am main, i will exit\n");
    }
    printf("======= main process end =======\n");
    
/**
孤儿进程的验证步骤:
ps -ajx 查询结果的表头:
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND

1、验证编译出来的a.out程序运行了两个进程
    ps -ajx | grep a.out | grep -v grep
2、查看a.out的进程号 pid
    ps -ajx | grep a.out | grep -v grep
3、进一步筛选与pid相关的进程信息
    ps -ajx | grep pid | grep -v grep
4、通过pid的关系可以看出:
    a.out主进程的父进程是 -bash    
    a.out子进程的父进程是 a.out的主进程
5、先结束父进程,观察子进程的PPID变化(由pid变成了1,即init进程号)
    ps -ajx | grep a.out | grep -v grep
    kill 父进程PID
    ps -ajx | grep a.out | grep -v grep

补充:
    kill -l  查看所有信号
    kill PID 结束进程号为PID的进程
*/
}

void test03(void)
{
    // 本来是希望利用for循环创建5个进程,结果创建了32个
    // 问题解决:在当前进程为子进程时,不执行fork即可
    printf("======= main process begin =======\n");
    pid_t pid;
    int i;
    
    for (i=0; i<5; i++)
    {
        pid = fork();
        if (pid == -1)
        {
            perror("fork error");
            return;
        }
        if (pid == 0) // 子进程
        {
            printf("i am child: %d, my parent: %d\n", getpid(), getppid());
            // break; // 注释解开时创建5个进程,不解开时创建32个进程
        }
        if (pid > 0) // 父进程
        {
            printf("i am main: %d, my child: %d, my parent: %d\n", getpid(), pid, getppid());
        }
    }
    while (1)
    {
        sleep(1);
    }
    
    if (pid > 0)
    {
        printf("i am main, i am waiting child\n");
        wait(&pid); // 防止僵尸进程的出现
        printf("i am main, i will exit\n");
    }
    printf("======= main process end =======\n");
    
/**
查看程序创建的进程个数:
    ps -ajx | grep a.out | grep -v grep | wc -l
结束程序a.out
    pkill a.out
*/
}

int main(void)
{
    test01(); // 验证复制创建进程
    // test02(); // 验证孤儿进程
    // test03(); // 控制进程创建个数
    
    return 0;
}
/** 运行结果:
======= main process begin =======
i am main: 17688, my child: 17689, my parent: 10139
i am main: uid[1000] euid[1000] gid[1000] egid[1000]
i am child: 17689, my parent: 17688
i am child: uid[1000] euid[1000] gid[1000] egid[1000]
main check res: 8
main check res: 6
child check res: 11
main check res: 4
child check res: 12
main check res: 2
main check res: 0
child check res: 13
main check res: -2
i am main, i am waiting child
child check res: 14
child check res: 15
child check res: 16
child check res: 17
child check res: 18
child check res: 19
child check res: 20
child check res: 21
======= main process end =======
i am main, i will exit
======= main process end =======
*/

exec函数族

exec函数族用于进程程序替换,子进程执行的是父进程的代码片段,那么当我们想让创建出来的子进程执行全新的程序时怎么办呢?这个时候我们就需要使用进程的程序替换了。
那我们为什么要进行程序替换呢?其实也不难理解,大概可以分为两点:

  • 我们想让子进程执行一个全新的程序;
  • 完成不同语言编写的程序间可以互相调用。

一般在进行服务器设计(Linux编程)的时候,往往需要子进程干两类事情:

  • 子进程执行父进程的代码段(服务器代码)
  • 子进程执行磁盘中一个全新的程序(shell让客户端执行对应的程序,通过我们的进程去执行其他人写的进程代码等,编程语言可以由 C/C++ -》 C/C++/Python/Shell/Java …)
程序替换为什么使用子进程?

注意:进行程序替换的是子进程!!!原因如下:

  • 进程替换永远影响的是进程的本身,子进程的替换永远不会影响父进程,因为进程具有独立性;
  • 独立性体现在内核层面,不同进程有不同的地址空间,有不同的页表替换只是加入新的代码和数据;
  • 重新建立的是页表映射但并不影响内核数据结构的具体情况;
  • 子进程虽然和父进程代码共享数据写实拷贝,但是一旦发生进程替换了,就认为代码和数据发生了双写实拷贝,就彻底将两个进程分开了;
  • 引入子进程的原因就是,一方面把需求做到位,另一方面不影响父进程,因为父进程可能还要接收新的命令,再去执行新的程序。
exec函数族的六个进程替换函数 && system函数
// 根据PATH环境变量寻找待执行程序,成功不返回(因为去执行程序了),失败返回-1;因为只有失败才返回,错误值-1,所以通常我们直接在exec函数调用后直接调用perror(),和exit(),无需if判断
// 参数1 程序名
// 参数2 argv0
// 参数3 argv1
// ... argvN
// 最后 NULL
// 区别:execlp(),让当前进程或者子进程执行系统命令,比如:ls,cat,cp等命令,而execl()则是执行自己所有的可执行程序,比如一个c程序a.out
// 示例:execl("/bin/echo", "echo", "Hello World", NULL);
int execl(const char *path, const char *arg, ...);

// 执行程序file,成功返回0,失败返回-1
// 示例:execlp("ls", "ls", "-l", NULL);
int execlp(const char *file, const char *arg, ...);

// 与execlp()函数不同的是,execle()函数可以显式地指定新程序的环境变量数组
// 示例:char *env_init[] = {"XX=xx", "OO=oo", NULL}; execle("./echoenv", "echoenv", NULL, env_init);
int execle(const char *path, const char *arg, ..., char *const envp[]);

// 执行指定路径下可执行文件的函数。该函数会将当前进程替换为指定路径下的可执行文件,并传递给新程序一个参数列表
// path:表示要执行的可执行文件的路径名
// argv:参数列表,是一个字符串数组,其中每个元素都是一个参数。最后一个元素必须为 NULL,用于标记参数列表的结束 
// 示例:char *argv[]={"ls", NULL, NULL}; execv("/bin/ls", argv);
int execv(const char *path, char *const argv[]);

// 在系统的 PATH 环境变量指定的路径中搜索可执行文件,当调用execvp 函数时,系统将自动搜索可执行文件并执行它。新程序接收到的命令行参数将由 argv 提供。可以通过遍历 argv 数组来获取传递给新程序的参数
// 示例:char *argv[]={"ls", "-l", NULL}; execvp("ls", argv);
int execvp(const char *file, char *const argv[]);

// 示例:extern char **environ; char *const argv_[]={"ls", "-l", NULL}; execvpe("ls", argv_, environ);
// 规律:l(list)表示参数采用列表v(vector)参数用数组
; p(path)有p自动搜索环境变量PATH
; e(env)表示自己维护环境变量
int execvpe(const char *file, char *const argv[], char *const envp[]);

// 通过调用/bin/sh-c命令执行命令中指定的命令,并在命令完成后返回。在命令执行期间SIGCHLD将被阻塞,
并且SIGINT和SIGQUIT将被忽略
// 失败返回-1,成功就执行命令;如果命令为NULL,shell可用system()返回非零,如果不可用则返回零
// system()不会影响任何其他子项的等待状态,通过源码可以看出Linux系统下,system函数是execl函数的封装版
// 示例:system("top");
#include 
int system(const char *command);

你可能感兴趣的:(Linux_C语言,linux,c语言,服务器,开发语言)