Linux 系统编程-(二)进程控制

一.进程

1.1.程序和进程的关系

简单来说,程序是静止的,就是我们的可执行文件,进程是动态的,就是运行起来的程序。

1.2.并行和并发

1)并行,parallel 强调同一时刻同时执行
2)并发,concurrency 则指的一个时间段内去一起执行

1.3.进程的状态

在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态,如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U06gvlln-1670279184431)(https://image-static.segmentfault.com/345/619/34561997-5fe986fd74426)]

1.4.进程各个状态的切换时机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-di4HxukL-1670279184432)(/img/bVcTPPA)]

①TASK_RUNNING(运行态):进程正在被CPU执行。
当一个进程刚被创建时会处于TASK_RUNNABLE,表示己经准备就绪,正等待被调度。

②TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞)等待某些条件的达成。
当条件达成,内核就会把进程状态设置为运行。
处于此状态的进程会因为接收到信号而提前被唤醒,比如给一个TASK_INTERRUPTIBLE(可中断)状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASK_RUNNABLE运行状态),然后再响应SIGKILL信号而退出(变为TASK_ZOMBIE停止状态),而不是从TASK_INTERRUPTIBLE状态(可中断)直接退出。

③TASK_UNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源满足时被唤醒,但不可以由其它进程通过信号或中断唤醒。
由于不接受外来的任何信号,因此无法用kill杀掉这些处于该状态的进程,这个状态存在的作用就是因为内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程,于是原有的流程就被中断了,这可能使某些设备陷入不可控的状态。
另外处于TASK_UNINTERRUPTIBLE状态一般情况下,是非常短暂的,很难通过ps命令捕捉到。

④TASK_ZOMBIE(僵死):表示进程已经结束了,但是其父进程还没有调用wait4或waitpid()来释放进程描述符。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。

⑤TASK_STOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候,还有在调试的时候接收到任何信号,也会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE。

1.5 进程号

进程号PID:每个进程都由一个进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。
父进程号PPID:任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组号PGID:进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)

1.6 进程相关函数

getpid函数

#include <sys/types.h>
#include <unistd.h>
​
pid_t getpid(void);
功能:
    获取本进程号(PID)
参数:
    无
返回值:
    本进程号

getppid函数

#include <sys/types.h>
#include <unistd.h>
​
pid_t getppid(void);
功能:
    获取调用此函数的进程的父进程号(PPID)
参数:
    无
返回值:
    调用此函数的进程的父进程号(PPID)

getpgid函数

#include <sys/types.h>
#include <unistd.h>
​
pid_t getpgid(pid_t pid);
功能:
    获取进程组号(PGID)
参数:
    pid:进程号
返回值:
    参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号

二.创建进程

2.1 fork函数

#include <sys/types.h>
#include <unistd.h>
​
pid_t fork(void);
功能:
    用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
    无
返回值:
    成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
    失败:返回-1。
    失败的两个主要原因是:
        1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
        2)系统内存不足,这时 errno 的值被设置为 ENOMEM。

使用 fork() 函数得到的子进程会将父进程复制一份(包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等),
所以使用fork()创建进程开销是很大的。

那么如何区分父进程和子进程呢?那就是根据fork()返回的pid值。
fork() 函数被调用一次,但返回两次,
子进程的返回值是 0,而父进程的返回值则是新子进程的进程 pid。

pid_t pid = fork(); // 根据这个返回值来区分父子进程,如同这个值小于0,那就是表示创建进程失败。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
    // 打印创建进程的进程号
    printf("创建子进程前的pid:[%d]\n",getpid());

    // 创建子进程
    pid_t pid = fork();
    
    if(pid < 0){// 创建进程失败
        perror("fork ");
        return -1;
    }else if(pid >0){ // 父进程执行内容
        printf("父进程执行[pid %d][ppid:%d\n]",getpid(),getppid());
        
    }else if(pid == 0){ //子进程执行
        printf("子进程执行[pid %d][ppid:%d\n]",getpid(),getppid());
    }
    printf("创建子进程后执行 pid[%d]\n",getpid());

    return 0;
}

输出结果

创建子进程前的pid:[29871]
父进程执行[pid 29871][ppid:29848
]创建子进程后执行 pid[29871]
子进程执行[pid 29872][ppid:29871
]创建子进程后执行 pid[29872]

父子进程中各自的空间是独立的,假定全局变量a,在子进程中修改a的值时,并不影响父进程。

2.2 exec 函数族

在运行的进程中,启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程,例如,我们执行一个程序,需要调用外部的ls命令,获取一个目录的资源信息,这里使用的就是exec函数族。
exec 函数族,就是一簇函数,在 Linux 中,并不存在 exec() 函数,exec 指的是一组函数,共有6个,如下:


#include <unistd.h>
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[]);
​
int execve(const char *filename, char *const argv[], char *const envp[]);

一般常用的有两个,execl和excelp。
execl函数一般执行自己写的程序

int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
path: 要执行的程序的路径
变参arg: 要执行的程序的需要的参数,一般先是程序的名字,之后是程序的执行参数
参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;
若是失败,会执行execl后面的代码,可以用perror打印错误原因。

参考示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
    printf("===============start ls=================");
    /*调用系统的ls命令*/
    execl("/bin/ls","ls","-l",NULL);
    perror("execl"); 

    return 0;
}

我们也可以自己写一个程序文件,来进行调用
例如我们写一个示例程序test.c,打印接受的参数

#include <stdio.h>
#include <unistd.h>

int man(int argc, char*argv[]){
    for(int i=0;i<argc;i++){
        printf("[%s] ",argv[i]);
    }
    return 0;
}

主程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
    printf("===============start ls=================");
    execl("./test","test","hello","world",NULL);
    perror("execl"); 

    return 0;
}

execlp很是相似,execlp函数一般是执行系统自带的程序或者是命令.

int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
参数说明:
   file: 执行命令的名字, 根据PATH环境变量来搜索该命令,
   其余和execl相同。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
    printf("===============start ls=================");
    /*调用系统的ls命令*/
    execlp("ls","ls","-l",NULL);
    perror("execl"); 

    return 0;
}

3 退出进程

3.1 资源回收

在进程退出的时候,内核会释放进程所有的资源,但仍然有一些资源不能被释放,主要指进程控制块PCB(进程号、退出状态、运行时间等)。
父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程,避免资源的浪费。

3.2 孤儿进程和僵尸进程

1)孤儿进程
父进程运行结束,但它的子进程却还未结束运行,这个子进程就成了孤儿进程。
内核会把孤儿进程的父进程设置为init ,而init 进程会循环地 wait() 它的已经退出的子进程。
所以孤儿进程并不会造成什么危害。

2)僵尸进程
若子进程结束运行,父进程还在继续运行,但是父进程没有调用wait或waitpid函数完成对子进程的资源回收,那么这个子进程就成了僵尸进程。

僵尸进程会占用系统的资源,霸占进程号,而进程号是有限的,过多的僵尸进程出现,会导致可用进程号减少,最后无法创建新的进程,所以要避免产生僵尸进程。

3.3 回收进程资源的函数wait或waitpid

1)wait

#include <sys/types.h>
#include <sys/wait.h>
​
pid_t wait(int *status);
功能:
    等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
    status : 进程退出时的状态信息。
返回值:
    成功:已经结束子进程的进程号
    失败: -1
    
具体说明:
wait()函数的主要功能为回收已经结束子进程的资源,调用 wait() 函数的进程会处于阻塞状态。

如果调用进程没有子进程,那函数立即返回;
如果它的子进程已经结束,则函数会立即返回,同时会回收那个该进程的资源。


如果传入参数 status 的值不是 NULL,那么wait() 就会把子进程退出时的状态取出并存入一个整数值(int),表明子进程是正常退出还是异常终止。

这个退出信息在一个 int 中包含了多个字段,我们使用用宏定义取出其中的每个字段,主要有如下三组宏信息

1) WIFEXITED(status) 为非0,表示进程正常结束

WEXITSTATUS(status) 如上宏为真,使用此宏可以获取进程退出状态 (exit的参数)

2) WIFSIGNALED(status)为非0,表示进程异常终止

WTERMSIG(status)如上宏为真,使用此宏,取得使进程终止的那个信号的编号。

3) WIFSTOPPED(status)为非0 → 进程处于暂停状态

WSTOPSIG(status)如上宏为真,使用此宏取得使进程暂停的那个信号的编号。

WIFCONTINUED(status)为非0,意味着进程暂停后已经继续运行

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main(){
    pid_t pid = fork();
    if(pid<0){
        perror("fork");
        return -1;
    }else if(pid >0){
        // 父进程调用wait 一次wait只能终止一个子进程
        int status;
        pid_t cpid = wait(&status);
        printf("child pid [%d]",cpid);
        if(WIFEXITED(status)){
            // 正常退出
            printf("child process normal exit. status[%d]\n",WEXITSTATUS(status));
        }else if(WIFSIGNALED(status)){
            // 被信号杀死
            printf("child process killed by signal, signo[%d]\n", WTERMSIG(status));
        }

    }else if(pid == 0){
        // 子进程
        for(int i=0;i<10;i++){
            printf("helloworld\n");
        }
        return 10;

    }

    return 0;
}

2)waitpid

#include <sys/types.h>
#include <sys/wait.h>
​
pid_t waitpid(pid_t pid, int *status, int options);
功能:跟wait一样,等待子进程终止,回收子进程的资源。
​
参数:
    pid : 参数 pid 的值有以下几种类型:
      pid > 0  等待进程 ID 等于 pid 的子进程。
      pid = 0  等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
      pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
      pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值,很少使用。
​
    status : 进程退出时的状态信息。和 wait() 用法一样。
​
    options : options 提供了一些额外的选项来控制 waitpid()。
            0:同 wait(),阻塞父进程,等待子进程退出。
            WNOHANG:没有任何已经结束的子进程,则立即返回。
            WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
                 
返回值:
    waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
        1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
        2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;
        3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;

代码示例(上面代码简单修改一下)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main(){
    pid_t pid = fork();
    if(pid<0){
        perror("fork");
        return -1;
    }else if(pid >0){
        // 父进程调用wait 一次wait只能终止一个子进程
        int status;
        pid_t cpid = waitpid(-1,&status,0);
        printf("child pid [%d]",cpid);
        if(WIFEXITED(status)){
            // 正常退出
            printf("child process normal exit. status[%d]\n",WEXITSTATUS(status));
        }else if(WIFSIGNALED(status)){
            // 被信号杀死
            printf("child process killed by signal, signo[%d]\n", WTERMSIG(status));
        }

    }else if(pid == 0){
        // 子进程
        for(int i=0;i<10;i++){
            printf("helloworld\n");
        }
        return 10;

    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值