linux系统编程(七)进程(下)

简介: linux系统编程(七)进程

1.3.2 getpid函数


获取当前进程ID

pid_t getpid(void);


1.3.3 getppid函数


获取当前进程的父进程ID

pid_t getppid(void);

区分一个函数是“系统函数”还是“库函数”依据:

② 是否访问内核数据结构

② 是否访问外部硬件资源  二者有任一 → 系统函数;二者均无 → 库函数


1.3.4 getuid函数


获取当前进程实际用户ID
uid_t getuid(void);
  获取当前进程有效用户ID
uid_t geteuid(void);


1.3.5 getgid函数


获取当前进程使用用户组ID
gid_t getgid(void);
  获取当前进程有效用户组ID
gid_t getegid(void);


1.3.6 进程共享

父子进程之间在fork后。有哪些相同,那些相异之处呢?
刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
父子不同处: 1.进程ID   2.fork返回值   3.父进程ID    4.进程运行时间    5.闹钟(定时器)   6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。   
练习:编写程序测试,父子进程是否共享全局变。              【fork_shared.c】
重点注意!躲避父子进程共享全局变量的知识误区!
【重点】:父子进程共享:1. 文件描述符(打开文件的结构体)  2. mmap建立的映射区 (进程间通信详解)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int var =100;
int main()
{
        pid_t pid;
        pid = fork();
        if(pid == -1){
                perror("fork error");
                exit(1);
        }else if(pid>0)
        {
                var = 288;
                printf("parent ,var = %d\n",var);
                printf("i'am parent pid = %d,getppid =%d\n",getpid(),getppid());
        }
        else if(pid==0)
        {
                var = 200;
                printf("i'am child pid =%d,ppid =%d\n",getpid(),getppid());
                printf("child,var = %d\n",var);
        }
        printf("-------finish----------\n");
        return 0;
}


特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。


1.3.7 gdb调试

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。
默认跟踪父进程。
set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程。
注意,一定要在fork函数调用之前设置才有效。          【follow_fork.c】

1.4 exec函数族


fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。


其实有六种以exec开头的函数,统称exec函数:

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 execve(const char *path, char *const argv[], char *const envp[]);(只有它是系统调用)


1.4.1 execlp函数


加载一个进程,借助PATH环境变量      
int execlp(const char *file, const char *arg, ...);  成功:无返回;失败:-1
    参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
    该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc,char *argv[]){
        pid_t pid =fork();
        if(pid==-1){
        perror("fork error");
        exit(1);
        }
        else if(pid ==0 )
        {
                //execlp("ls","ls","-l","-d","-h",NULL);
                execlp("date","date",NULL);
                perror("exec error");
                exit(1);
        }else if(pid >0)
        {
                sleep(1);
                printf("I'am parent :%d\n",getpid());
        }
}


1.4.2 execl函数


加载一个进程, 通过 路径+程序名 来加载。 
    int execl(const char *path, const char *arg, ...);  成功:无返回;失败:-1
对比execlp,如加载"ls"命令带有-l,-F参数
execlp("ls", "ls", "-l", "-F", NULL);      使用程序名在PATH中搜索。
execl("/bin/ls", "ls", "-l", "-F", NULL);    使用参数1给出的绝对路径搜索。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc,char *argv[]){
        pid_t pid =fork();
        if(pid==-1){
        perror("fork error");
        exit(1);
        }
        else if(pid ==0 )
        {
                //execlp("ls","ls","-l","-d","-h",NULL);
               // execlp("date","date",NULL);
               // execl("./a.out","./a.out",NULL);
                //execl("./a.out","./a.out",AA,BB,CC);
                 execl("/bin/ls","ls","-l",NULL);
                perror("exec error");
                exit(1);
        }else if(pid >0)
        {
                sleep(1);
                printf("I'am parent :%d\n",getpid());
        }
}
~                                                                                                  
~                                                                                                  
~


1.4.3 execvp函数


加载一个进程,使用自定义环境变量env
int execvp(const char *file, const char *argv[]);
变参形式: 
①... 
② argv[]  (main函数也是变参函数,形式上等同于 int main(int argc, char *argv0, ...)) 
变参终止条件:① NULL结尾 ② 固参指定
execvp与execlp参数形式不同,原理一致。
练习:将当前系统中的进程信息,打印到文件中。            【exec_ps.c】
...
char *argv[]={"ls","-l","-h",NULL}
execvp("ls",argv);
...


1.4.4 exec函数族一般规律


exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。

l (list)    命令行参数列表
p (path)    搜素file时使用path变量
v (vector)    使用命令行参数数组
e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量


事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

1670989340707.jpg

exec函数族


1.5 回收子进程


1.5.1孤儿进程

孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
【orphan.c】
1
2
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void){
  pid_t pid;
  pid= fork();
  if(pid == 0){
  while(1){
    printf("i am child, my parent pid = %d\n",getppid());
    sleep(1);
  }
  }else if(pid > 0){
  printf("i am parent ,my pid is = %d\n",getpid());
  sleep(9);
  printf("----------parent going to die ------------\n");
  }else{
  perror("fork");
  return 1;
  }
}


1.5.2 僵尸进程


僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?杀死父进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
  pid_t pid;
  pid =fork();
  if(pid == 0){
  printf("----child,my parent =%d,going to sleep 10s\n",getppid());
  sleep(10);
  printf("-----------------child die ----------------\n");
  }else if(pid > 0)
  {
  while(1){
    printf("i am parent ,pid = %d,myson = %d\n",getpid(),pid);
    sleep(1);
  }
  }else{
  perror("fork");
  return 1;
  }
}


1.5.3 wait函数


一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。


父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出 
② 回收子进程残留资源 
③ 获取子进程结束状态(退出原因)。
pid_t wait(int *status);  成功:清理掉的子进程ID;失败:-1 (没有子进程)


当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2.

释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号) 可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

1.  WIFEXITED(status) 为非0 → 进程正常结束
  WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
 2.  WIFSIGNALED(status) 为非0 → 进程异常终止
  WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
*3.  WIFSTOPPED(status) 为非0 → 进程处于暂停状态
  WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
  WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(void){
  pid_t pid,wpid;
  int status;
  pid = fork();
  if(pid == 0){
  printf("---child ,my id =%d,going to sleep 10s\n",getpid());
  sleep(10);
  printf("----------------child die---------------\n");
  return 73;
  }else if (pid > 0){
  //wpid = wait(NULL);  
  wpid = wait(&status);
  if(wpid == -1){
    perror("wait error");
    exit(1);
  }
  if(WIFEXITED(status)){
    printf("child exit with %d\n",WEXITSTATUS(status));
  }
  if (WIFSIGNALED(status)){
    printf("child kill with signal%d\n",WTERMSIG(status));
  }
  printf("-----------parent wait finish :%d\n",wpid);
  }else{
  perror("fork");
  return 1;
  }
  return 0;
  }


1.5.4waitpid函数


作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options);
    成功:返回清理掉的子进程ID;
    失败:-1(无子进程)
特殊参数和返回情况:
参数pid: 
> 0 回收指定ID的子进程  
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程
返回0:
参3为WNOHANG,且子进程正在运行。


注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。                            【waitpid.c】

一次调用,回收一个子进程

#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc,char *argv[])
{
        int i;
        pid_t pid,wpid,tmpid;
        for(i=0;i<5;i++){
                pid =fork();
                if(pid==0)
                {
                break;
                }
                if(i == 2){
                        tmpid = pid;
                        printf("------------pid = %d\n",tmpid);
                }
        }
        if(5==i){
                //sleep(5);
                //wait(NULL);
                //wpid = waitpid(-1,NULL,WNOHANG);
                //wpid = waitpid(tmpid,NULL,0);
                printf("i am parent,before waitpid ,pid =%d\n",tmpid);
                wpid = waitpid(tmpid,NULL,WNOHANG);
                if(wpid == -1){
                        perror("waitpid error");
                        exit(1);
                }
                printf("i am parent ,wait a child finish : %d\n",wpid);
        }else{
                sleep(i);
                printf("i'm %dth child,pid =%d\n",i+1,getpid());
        }
        return 0;
}


想回收多个。while

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc,char *argv[]){
  int i;
  pid_t pid,wpid;
  for(i=0;i<5;i++){
  pid = fork();
  if(pid == 0){
    break;
  }
  }
  if(5==i){
  /*
  *
  * while((wpid = waitpid(-1,NULL,0))){
    printf("wait child %d \n",wpid);
  }
  * 
  */
  while((wpid = waitpid(-1,NULL,WNOHANG))!=-1){
  if(wpid>0){
    printf("wait child %d\n",wpid);
  }
  else if(wpid == 0)
  {
    sleep(1);
    continue;
  }
  }
  }else{
  sleep(i);
  printf("i'm %dth child ,pid = %d\n",i+1,getpid());
  } 
}


作业:父进程fork 3 个子进程,三个子进程一个调用ps命令, 一个调用自定义程序1(正常),一个调用自定义程序2(会出段错误)。父进程使用waitpid对其子进程进行回收。

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
1月前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第7天】麒麟系统mate-indicators进程占用内存过高问题解决
156 2
|
11天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
39 4
linux进程管理万字详解!!!
|
2天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
33 8
|
10天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
38 4
|
11天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
13天前
|
消息中间件 存储 Linux
|
16天前
|
运维 监控 Shell
深入理解Linux系统下的Shell脚本编程
【10月更文挑战第24天】本文将深入浅出地介绍Linux系统中Shell脚本的基础知识和实用技巧,帮助读者从零开始学习编写Shell脚本。通过本文的学习,你将能够掌握Shell脚本的基本语法、变量使用、流程控制以及函数定义等核心概念,并学会如何将这些知识应用于实际问题解决中。文章还将展示几个实用的Shell脚本例子,以加深对知识点的理解和应用。无论你是运维人员还是软件开发者,这篇文章都将为你提供强大的Linux自动化工具。
|
19天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
19 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
20 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
29 0
Linux c/c++之IPC进程间通信