• 【Linux 应用编程】进程管理


    基本概念

    程序和进程的区别

    程序是平台相关的二进制文件,只占用磁盘空间。编写完程序代码后,编译为可执行的二进制文件即可。

    进程是运行中的程序,占用 CPU、内存等系统资源。

    通过 Shell 命令,可以在终端启动进程,例如执行 ls 命令:

    • 找到命令对应的二进制文件
    • 使用 fork() 函数创建新的进程
    • 在新创建的进程中调用 exec 函数组,加载命令对应的二进制文件,并从 main 函数开始执行

    并发和并行

    并发 concurrent:在一个时间段内,处理的请求总数。个数越多,并发越大。
    并行 parallel:任意时刻能够同时处理的请求数。通常跟 CPU 内核数量相关。

    Linux 进程的状态

    Linux 的进程有以下 6 种状态:

    • D:深度睡眠状态,不可中断,处于这种状态的进程不能响应异步信号
    • R:进程处于运行态或就绪状态,只有在该状态的进程才可能在 CPU 上运行
    • S:可中断的睡眠状态,处于这个状态的进程因为等待某种事件的发生而被挂起
    • T:暂停状态或跟踪状态
    • X:退出状态,进程即将被销毁
    • Z:退出状态,进程成为僵尸进程

    进程状态转换图

    命令行控制进程

    ps 命令

    通过 ps aux 可以查看当前机器上的进程,其中 STAT 列的第一个字符就是进程的状态:

    # ps aux
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.2 190764  2140 ?        Ss    2018  12:35 /usr/lib/systemd/systemd --system --deserialize 20
    root         2  0.0  0.0      0     0 ?        S     2018   0:00 [kthreadd]
    root         3  0.0  0.0      0     0 ?        S     2018   1:24 [ksoftirqd/0]
    

    kill 命令

    kill 命令用于结束进程,语法如下:

    kill [-s signal|-p] [-q sigval] [-a] [--] pid...
    kill -l [signal]
    

    执行 kill 命令,系统会发送一个 SIGTERM 信号给对应的进程,请求进程正常关闭。SIGTERM 是有可能会被阻塞的。kill -9 命令用于强制杀死进程,系统给对应程序发送的信号是 SIGKILL,即 exit。exit 信号不会被系统阻塞。
    示例:

    kill -9 11235
    

    PID

    每个进程在创建的时候,内核都会为之分配一个全局唯一的进程号。

    getpid 和 getppid 函数

    通过 getpid 函数可以获取当前进程的 PID。getppid 函数可以获取父进程的 PID。

    通过 ps aux 可以查看进程的 PID,资源消耗情况,通过 ps -ef 可以查看当前进程及其父进程的 PID。通过 pstree 命令可以以树状关系查看所有进程。

    函数原型:

    #include <sys/types.h>
    #include <unistd.h>
    
    pid_t getpid(void);
    pid_t getppid(void);
    

    环境变量

    除了通过 main 函数的第三个参数获取环境变量,还可以通过 environ 全局变量或 getenv() 函数来获取。

    getenv 函数原型:

    #include <stdlib.h>
    
    char *getenv(const char *name);
    char *secure_getenv(const char *name);
    

    环境变量示例

    #include <stdio.h>
    #include <stdlib.h>
    
    extern char** environ;
    
    int main(int argc, char* argv[], char* env[])
    {
        int i = 0;
        char* myenv = NULL;
        while(env[i])
        {
            printf("env[%d] is: %s
    ", i, env[i++]);
        }
        i = 0;
        while(environ[i])
            puts(environ[i++]);
    
        myenv = getenv("PATH");
        puts(myenv);
        return 0;
    }
    

    孤儿进程和僵尸进程

    PCB(Process Control Block,进程控制块)是每个进程都有的数据结构,用于保存进程运行所需的信息,例如文件描述符表。

    wait() 函数用来帮助父进程获取其子进程的退出状态。当进程退出时,内核为每一个进
    程保存了退出状态信息。如果父进程未调用 wait() 函数,则子进程的退出信息将一直保存在内存中。

    孤儿进程

    Linux 中,每个进程在退出的时候,可以释放用户区空间,但是无法释放进程本身的 PCB 所占用的内存资源。PCB 必须由父进程释放。

    父进程在创建子进程后退出,子进程变成孤儿进程。为防止内存泄漏,孤儿进程被 init 进程领养,init 进程变成孤儿进程的父进程。

    下面示例中,父进程先退出:

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    
    int main()
    {
    	pid_t pid = fork();
    	
    	if (pid == 0) 
    	{
    		printf("child pid is: %d, ppid is: %d
    ", getpid(), getppid());
    		sleep(1);
    		printf("child pid is: %d, ppid is: %d
    ", getpid(), getppid());
    	}
    	else if (pid > 0)
    	{	
    		sleep(0.5);
    		printf("parent pid is: %d, ppid is: %d
    ", getpid(), getppid());
    		printf("parent exit
    ");
    	}
    	return 0;
    }
    

    输出:

    parent pid is: 3348, ppid is: 713
    parent exit
    child pid is: 3349, ppid is: 1
    child pid is: 3349, ppid is: 1
    
    

    僵尸进程

    子进程退出了,父进程一直未调用 wait 或 waitpid 函数,子进程就变成了僵尸进程。

    代码控制进程

    进程入口函数 main

    可执行的二进制文件,都是从 main 函数开始执行的。main 函数有 3 种原型定义:

    int main();
    int main(int argc, char *argv[]);
    int main(int argc, char *argv[], char *env[]);
    

    参数:

    • argc:参数个数
    • argv:参数数组,每个参数都是字符串
    • env:环境变量数组,每个环境变量都是字符串

    进程基本操作

    注意,Shell 终端无法检测进程是否创建了子进程。在进程执行完毕后,Shell 会立即回到交互状态,此时如果子进程还在输出数据,会打印在 Shell 的命令提示符之后。可以在父进程中 sleep 一下。

    创建进程

    Linux 中用 fork() 函数创建新进程,函数原型如下:

    #include<unistd.h>
    
    pid_t fork(void);
    

    返回值:
    成功创建进程时,会对父子进程各返回一次,对父进程返回子进程的 PID,对子进程返回 0。通过条件分支语句可以分别进行不同处理。失败则返回小于 1 的错误码。

    fork 函数执行的时候,会将当前正在运行的进程完整的复制一份,提供给子进程。子进程从 fork 函数之后开始执行

    创建多个子进程

    通过 for 循环,可以创建多个子进程,只需要在每次 fork 之后判断如果是子进程则结束循环即可。通过循环下标可以判断当前子进程是第几次创建的:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
    	int i = 0;
    	int number = 5;
    	pid_t pid;
    	
    	for (i = 0; i < number; i++)
    	{
    		pid = fork();
    		if (pid == 0) break;
    	}
    	
    	// 子进程
    	if (i == 0) printf("first process, pid = %d
    ", getpid());
    	if (i == 1) printf("second process, pid = %d
    ", getpid());
    	//...
    	
    	// 父进程
    	if (i == number) printf("parent process, pid = %d
    ", getpid());
    	return 0;
    }
    

    终止进程

    进程的终止分为两种:

    • 正常终止:
      • 从 main() 函数中 return 返回,实际上也是调用的 exit() 函数
      • 调用 exit() 类函数
    • 异常终止:
      • 调用 abort() 函数
      • 接收到终止信号

    exit 函数原型如下:

    #include <stdlib.h>
    
    void exit(int status);
    

    exit(0) 等价于 return 0

    exec 函数组

    fork 函数可以复制一份父进程,得到的子进程跟父进程有完全一样的代码跟数据。之后两个进程各自执行,互不影响。

    实际上我们通常需要子进程执行不同的代码,这时就需要通过 exec 函数加载代码段,并跳转到新代码段的 main 入口执行。

    函数原型:

    #include <unistd.h>
    
    extern char **environ;
    
    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[]);
    

    exec 函数组是在 exec 上加 l、v、p、e 四个后缀形成的,这些函数作用相同,只是在参数列表上存在差别。

    • 后缀 p:用文件名做参数,如果文件名中不含路径则会去 PATH 环境变量所指定的各个目录中搜索可执行文件。无后缀 p 则使用绝对路径名来指定可执行文件的位置。
    • 后缀 e:表示可以传递一个指向环境字符串指针数组的指针,环境数组需要以 NULL 结束。而无此后缀的函数则使用调用进程中 environ 变量为新程序复制现有的环境。
    • 后缀 l:用 list 形式来传递新程序的参数,传给新程序的所有参数以可变参数的形式传递,最后一个参数必须是 NULL。
    • 后缀 v:用 vector 形式来传递新程序的参数,传给新程序的所有参数放入一个字
      符串数组中,数组以 NULL 结束。

    返回值:exec 族函数报错时才有返回值 -1,否则无返回值。如果执行到后面的代码,就是出错了。

    示例:

    char* myenv[] = {"TEST=666", "HOME=/home/kika", NULL};
    execle("home/kika/test", "hello", "world", myenv);
    

    完整示例:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
    	int pid = fork();
    	if (pid > 0) {
    		exit(0);
    	} else if (pid == 0) {
    		execle("home/kika/test", "hello", "world", myenv);
    		perror("execle error");
    		exit(-1);
    	} else {
    		perror("fork error");
    	}
    	return -1;
    }
    

    wait 和 waitpid 函数

    通常,在父进程中调用 wait 函数,可以查看子进程的退出信息,让子进程撤单结束。wait 函数是阻塞式的,waitpid 可以设置为非阻塞的。父进程根据创建的子进程个数,在循环中通过 wait 函数逐个回收子进程。而 waitpid 函数则可以通过 PID 等待指定的子进程退出。

    wait 函数调用一次只会回收一个子进程。多个子进程需要调用多次 wait。

    函数原型:

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int options);
    

    wait 参数:

    • status:进程退出的原因。可以通过预定义的宏来检查:
      • WIFEXITED(status):若为正常终止子进程返回状态,则为真
      • WEXITSTATUS(status):获取子进程传给 exit 或 return 的参数
      • WIFSIGNALED(status):若为异常终止子进程返回状态,则为真
      • WTERMSIG(status):获取使子进程退出的信号

    waitpid 参数:

    • pid:用于设置 waitpid 工作方式。
      • -1:等价于 wait,等待任意子进程退出
      • 0:等待组 ID 等于调用进程的组 ID 的任一子进程退出
      • > 0:等待 PID 等于该数值的进程退出
      • < -1:等待其组 ID 等于该数值的任一子进程退出
    • options:用于设置 waitpid 是否阻塞执行。0 则阻塞,设为 WNOHANG 则非阻塞。

    返回值:成功时返回退出子进程的 PID,没有子进程时返回 -1.

    综合示例

    创建三个子进程,分别运行自定义程序,shell 程序,未定义程序(段错误)。然后在父进程中通过 wait 回收所有子进程,并分别判断退出原因:

    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <sys/stat.h>
    #include <errno.h>
    
    int main(int argc, char* argv[])
    {
        int num = 3;
        int i;
        pid_t pid;
        for (i = 0; i < 3; i++) {
            pid = fork();
            if (pid == 0) break;
        }
        
        if (i == 0) {
            execlp("ps", "ps", "aux", NULL);
            perror("execlp ps error");
            exit(1);
        } else if (i == 1) {
            execl("/root/test/api/process/myls", "", NULL);
            perror("execl myls error");
            exit(1);
        } else if (i == 2) {
            execl("./error", "", NULL);
            perror("execl ./error");
            exit(1);
        } else {
            int status;
            pid_t pid;
            while (pid = wait(&status) != -1) {
                printf("children PID is: %d
    ", pid);
                if (WIFEXITED(status)) {
                    printf("return value is: %d
    ", WEXITSTATUS(status));
                } else if (WIFSIGNALED(status)) {
                    printf("died by signal: %d
    ", WTERMSIG(status));
                }
            }
        }
        return 0;
    }
    
  • 相关阅读:
    Android 布局中设置分割线
    程序员的创意爱情告白大法
    java中路径/和\的区别
    Oracle解锁表
    Android组件TextView细节
    android this,getApplication(),getApplicationContext()的区别
    ORA01013: user requested cancel of current operation
    Android自定义View之一:初探实例
    使用开源jabber(XMPP)协议及openfire架设内部即时通讯服务
    九、为ASP.NET MVC应用程序创建单元测试
  • 原文地址:https://www.cnblogs.com/kika/p/10851501.html
Copyright © 2020-2023  润新知