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
相关文章
|
6天前
|
Shell Linux
Linux shell编程学习笔记30:打造彩色的选项菜单
Linux shell编程学习笔记30:打造彩色的选项菜单
|
7天前
|
Linux Shell
Linux系统
是对Linux系统进行管理的命令。对于Linux系统来说,无论是中央处理器、内存、磁盘驱动器、键盘、鼠标,还是用户等都是文件,Linux系统管理的命令是它正常运行的核心,与之前的DOS命令类似。linux命令在系统中有两种类型:内置Shell命令和Linux命令。
|
6天前
|
Ubuntu Linux 网络安全
从头安装Arch Linux系统
本文记录了作者安装Arch Linux系统的过程,包括安装成果展示和遇到的疑难点及其解决方法,如硬盘不足、下载失败、设置时区、安装微码和配置无密码登录等。
从头安装Arch Linux系统
|
3天前
|
Linux Shell
Linux系统
是对Linux系统进行管理的命令。对于Linux系统来说,无论是中央处理器、内存、磁盘驱动器、键盘、鼠标,还是用户等都是文件,Linux系统管理的命令是它正常运行的核心,与之前的DOS命令类似。linux命令在系统中有两种类型:内置Shell命令和Linux命令。
|
1天前
|
Ubuntu Linux
Linux系统基本操作
Linux系统基本操作
12 7
|
1天前
|
Linux Shell
Linux系统
是对Linux系统进行管理的命令。对于Linux系统来说,无论是中央处理器、内存、磁盘驱动器、键盘、鼠标,还是用户等都是文件,Linux系统管理的命令是它正常运行的核心,与之前的DOS命令类似。linux命令在系统中有两种类型:内置Shell命令和Linux命令。
|
6天前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
2天前
|
监控 安全 Linux
使用NRPE和Nagios监控Linux系统资源的方法
通过遵循以上步骤,可以有效地使用NRPE和Nagios监控Linux系统资源,确保系统运行稳定,并及时响应任何潜在的问题。这种方法提供了高度的可定制性和灵活性,适用于从小型环境到大型分布式系统的各种监控需求。
12 2
|
2天前
|
Linux Shell
Linux系统
是对Linux系统进行管理的命令。对于Linux系统来说,无论是中央处理器、内存、磁盘驱动器、键盘、鼠标,还是用户等都是文件,Linux系统管理的命令是它正常运行的核心,与之前的DOS命令类似。linux命令在系统中有两种类型:内置Shell命令和Linux命令。
|
3天前
|
Ubuntu Linux Shell
Linux系统密码忘记
【10月更文挑战第2天】在Linux系统中,若忘记密码,可以通过单用户模式或使用Live CD/USB来重置。对于Ubuntu系统,可通过GRUB引导菜单进入单用户模式,利用命令行重置密码;或使用Live CD/USB启动并挂载硬盘分区后修改密码文件。CentOS系统同样支持单用户模式重置密码,也可借助安装介质进入救援模式,挂载文件系统后进行密码重置。这些方法均能在忘记密码的情况下帮助恢复系统访问。
下一篇
无影云桌面