unix进程控制及进程环境--自APUE

概述

1、什么是进程

进程是程序的一次运行,是一个动态过程。

进程控制块PCB:内核中专门用于管理进程的数据结构,里面记录了进程的各种信息。

2、孤儿进程和僵尸进程

  • 僵尸进程

一个进程有两类资源,一个是运行时候向内核申请的资源,包括IO资源(比如open文件产生的IO资源)、内存资源(malloc产生的)等;第二个是进程自带的用于描述进程自身的资源(占8KB),包括描述进程相关信息的数据结构task_struct和栈内存,这类数据是进程创建之初就生成的。

一个进程退出后,内核会回收进程的运行时资源,但是8KB内存操作系统不会去回收,只能由其父进程去回收(谁创建谁负责回收的原则),这些资源被父进程通过wait函数获取后被释放,如果一个子进程退出后,父进程没有wait这些信息,那么这个子进程就变成了僵尸进程。

防止进程变成僵尸进程的几种方式:

  1. 父进程显示通过wait回收子进程的资源
  2. 父进程退出的时候会隐式的去回收子进程的资源
  3. 子进程退出时,内核会向父进程发送SIGCHILD,父进程处理该信号的时候通过wait获取退出信息
  4. 让进程被进程1接管,进程1会wait每一个退出的子进程
  • 孤儿进程

父进程退出后,子进程还没有退出,那么子进程就会被进程1接管变成孤儿进程。因此子进程可以通过下面的命令来判断父进程是否退出了:

while(getppid() != 1) sleep(1);

3、进程的分裂生长模式

内核加载一个进程的时候会有很多前置工作,如果创建一个新进程的时候,从头做一遍,效率会很低,所以内核采用了一种方法:把父进程的内容先原封不动的复制一份,然后做一些修改,比如修改pid号等,这样就能大大提高新进程创建的效率。

进程终止

进程的编译和启动

进程的编译链接:裸机编程需要写链接脚本,但是linux下编程就不用了,因为每个进程的链接方式都是固定的,gcc会自动把事先准备好的引导代码链接到main函数的前面,这段代码每个程序都是一样的。

进程的加载:进程运行的时候,加载器会把进程加载到内存中,然后去执行。

进程编译的时候使用链接器,运行的时候使用加载器

argc和argv的传参:shell下执行进程的时候,这俩参数首先被shell解析,然后传递给加载器,最后通过main函数传递给进程,所以我们在main函数中能使用这两个参数。

进程终止的步骤

img

进程启动的时候,内核会通过exec打开一个启动例程,这个启动例程通过下面的函数执行目标的进程,如果目标进程在main函数中return 0的时候,就相当于直接执行了exit(0),所以return之后执行的步骤和exit一样;

exit(main(argc, argv));

进程终止的几种场景如下:

  • 如果功能函数调用return:那么返回main函数;
  • 如果功能函数调用_exit或_Exit:那么直接进入到内核
  • 如果功能函数调用exit:那么执行终止处理函数、清理IO、删除临时文件后进入内核
  • 如果main函数调用return:那么返回启动例程,然后启动例程会调用exit,进入exit处理流程后进入内核
  • 如果main函数调用_exit或_Exit:同功能函数
  • 如果main函数调用exit:同功能函数

进程8种终止方式

正常终止方式:

  1. 从main返回
  2. 调用exit
  3. 调用_exit或_Exit
  4. 最后一个线程 从其启动例程返回
  5. 最后一个线程调用thread_exit

异常终止:

  1. 调用abort
  2. 接到一个信号
  3. 最后一个线程对取消请求做出响应

进程退出函数1:exit

这是一种进程正常退出的库函数,通过man 3 exit可以查看,exit函数没有返回值,会返回status状态码给调用他的父进程。

进程调用exit时候,会执行以下步骤:

  1. 先调用终止处理程序。终止处理程序通过atexit函数注册,一个进程最多注册32个。调用的顺序和注册的顺序相反。终止处理程序没注册一次会被调用一次,尽管是相同的函数注册了多次
  2. 所有打开的标准IO流会被flush和close
  3. 通过tmpfile创建的临时文件会被删除
#include <stdlib.h>
void exit(int status);
status:给父进程的返回码

进程退出函数2:_exit

_exit是一个系统调用,作用也是退出程序,但是不会去执行终止处理函数、清理IO、删除临时文件等步骤,直接进入到内核:

  1. 所有的文件描述符会被直接关闭
  2. 然后所有的子进程被进程1接管
  3. 给父进程传递SIGCHLD信号
#include <unistd.h>
void _exit(int status);
status:给父进程的返回码

进程退出函数3:_Exit

同_exit

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

注册终止处理程序:atexit

#include <stdlib.h>
int atexit(void (*function)(void));
返回值:成功,返回0,失败返回非0数字。

示例代码:

#include <stdio.h>
#include <stdlib.h>

static void test1(void)
{
        printf("test1\n");
}

static void test2(void)
{
        printf("test2\n");

}

int
main(int argc, char **argv)
{
        if (atexit(test1) != 0) {
                printf("atexist test1 failed.\n");
        }
        if (atexit(test2) != 0) {
                printf("atexist test2 failed.\n");
        }

        exit(1);
}

执行结果:

[root@localhost exit]# ./atexit 
test2
test1

环境变量

通过main函数传参

环境变量可以通过main函数直接传参进来,需要main函数按照以下格式定义。这种方式其实就是把全局环境变量表environ的地址传进来。

#include <stdio.h>
int
main(int argc, char **argv, char **envp)
{
        int i = 0;
        for (i = 0; i < argc; i++){
                printf("%s\n", argv[i]);
        }
        i = 0;
        while(envp[i]) {
                printf("%s\n", envp[i]);
                i++;
        }
        return 0;
}

全局的环境变量表:environ

进程打开之后会有一个默认的环境变量表,这个环境变量表是shell环境变量的一份拷贝,通过一个全局的指针数组可以访问到表里的内容:

#include <unistd.h>
extern char **environ;

获取环境变量表的示例代码如下,获取的结果和shell 命令env的结果基本一致:

#include <unistd.h>
#include <stdio.h>
extern char **environ;
int
main(int argc, char **argv)
{
        int i = 0;
        while(environ[i] != NULL) {
                printf("%d\t%s\n", i+1, environ[i]);
                i++;
        }
        return 0;
}

运行结果如下:

[root@localhost getenv]# vim environ.c ^C
[root@localhost getenv]# ./environ 
1       XDG_SESSION_ID=17
2       HOSTNAME=localhost.localdomain
3       RTE_INCLUDE=/usr/include/dpdk
4       TERM=xterm
5       SHELL=/bin/bash
6       HISTSIZE=1000

获取环境变量:getenv

环境变量都是key=value的格式,getenv在环境变量表中,查找key对应的value,返回指向value的指针(不会带上"key="),如果找不到返回NULL。

#include <stdlib.h>
char *getenv(const char *name);
name:环境变量的key;
成功,返回指向value指针,失败或者找不到返回NULL;

修改环境变量:putenv

作用如下:

  1. 如果环境变量不存在则添加环境变量
  2. 如果环境变量已经存在,那么把环境变量的值设置成最新的值。

注意事项:

  1. putenv了之后,string地址会添加到environ表中。
  2. putenv了之后,如果修改了string的内容,那么环境变量也会对应被修改。
  3. 环境变量的修改只会影响当前进程和子进程的环境变量表,对父进程无效
#include <stdlib.h>
int putenv(char *string);
string:必须是"key=value"的格式;
返回值:成功返回0,失败返回非0,并且置上errno;

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
extern char **environ;
int
main(int argc, char **argv)
{
        char buf[256] = "name=xiaoming";
        int i = 0;
        if (0 != putenv(buf)) {
                perror("putenv failed.\n");
                return 0;
        }
        printf("%s=%s\n", "name", getenv("name"));
        strcpy(buf, "name=xiaowang");
        printf("%s=%s\n", "name", getenv("name"));
        while(environ[i]) {
                printf("%s\n", environ[i]);
                i++;
        }
        return 0;
}

输出结果:

name=xiaowang

修改环境变量:setenv

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
作用:把name=value的环境变量加入到环境变量表中;
overwrite:当overwrite为0,如果环境变量已经存在,那么不会覆盖掉原来的值,如果环境变量不存在,则添加环境变量;当overwrite为非0,如果环境变量已经存在,那么覆盖掉原来的值,如果环境变量不存在,则添加环境变量;
返回值:成功,返回0,失败返回-1,并且置上errno;

删除环境变量

进程堆空间申请和释放

申请指定大小的内存:malloc

malloc用于申请指定大小的内存,返回指针指向申请的内存。如果size的值为0,返回值是NULL或者是一个特定值得指针,这个指针可以作为free函数的参数被释放而不会报错。

#include <stdlib.h>
void *malloc(size_t size);
size:申请内存以字节为单位的大小;
返回指向新申请内存的指针

申请初始化的内存:calloc

calloc用于申请一段指定大小、指定数目的内存,该内存会被初始化成0,如果大小和数目为0,返回值是NULL或者是一个特定值得指针,这个指针可以作为free函数的参数被释放而不会报错。

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
nmemb:内存数目;
size:内存大小;
返回新申请内存的指针

修改已申请内存大小:realloc

realloc用于修改已申请内存的大小,ptr指向修改前的内存,size设置修改后的内存大小,可以改大也可以改小,大体分为以下几个场景:

  1. 如果改小:那么修改前的内存的起始地址到size大小的范围内的数据不会被修改,返回指向修改前的内存指针
  2. 如果改大,将在原来堆地址继续往高地址空间扩展,有两种可能,1)一是空间连续且足够,那么返回原来的空间地址,注意新增加的内存不会被初始化;2)二是连续的空间不够,那么寻找新的地址空间,并将原来的数据转移到新的空间中,原来的内存会被自动释放掉,返回新的空间地址

ptr指针和size之间的组合关系大体分为以下几种情况:

  1. ptr等于NULL:realloc等效于malloc()
  2. ptr不等于NULL:ptr必须是通过malloc(), calloc(), realloc()分配过的
  3. size等于0:realloc等效于free()
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
ptr:NULL或者指向修改前的内存;
size:修改后的内存大小;
返回值:修改成功返回新的内存地址,修改失败返回NULL,此时原来的ptr还可以继续用,数据也不会被修改;

进程ID

实现原理就是从进程控制块PCB中吧对应的数据结构获取出来

获取进程ID:getpid

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
返回值: 这个函数永远返回成功,返回调用进程的ID;

获取父进程ID:getppid

#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
返回值: 这个函数永远返回成功,返回调用进程的父进程ID;

子进程

创建子进程:fork

fork()系统调用用于创建一个子进程,子进程和父进程之间的关系为:

  • 子进程对数据、堆、栈都复制了一份副本,子进程有独立的PCB,父子进程对数据的访问互相不影响,
  • 子进程复制父进程文件描述符,但是指向同一个文件表项,共享文件偏移量,所以父子进程写同一个文件会相互影响,并且父进程或子进程return了之后,fd对应的文件会被关闭,会影响到另一个进程对文件的访问。
  • 父子进程返回值不同
  • 父子进程ID不同
  • 子进程不继承父进程的内存锁和记录锁
  • 子进程不继承父进程的定时器
  • 子进程的signal会被清空
  • 父进程退出,子进程未退出,子进程被init进程收养
  • 子进程退出,其信息未被父进程通过wait函数收集,子进程会变成僵死进程
  • 子进程加入系统进程调度,且与父进程独立,所以子进程和父进程的运行顺序是随机的
#include <unistd.h>
pid_t fork(void);
返回值:父进程返回子进程的进程ID,子进程返回0;如果创建失败,父进程返回-1,并且置上errno,没有子进程被创建;

创建子进程:vfork

vfork也是创建一个子进程,和fork之间的区别在于:

  1. 子进程和父进程之间共享内存数据,包括代码段、数据段、堆、栈等,创建子进程的效率比fork高
  2. 子进程先执行,父进程会卡主,直到子进程调用了exit或execve(不能是调用return),父进程继续运行

应用场景:很多时候创建子进程只是为了执行exec,这种场景下,没必要对父进程所有的数据都进行复制,用vfork效率会更高。

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
返回值:父进程返回子进程的进程ID,子进程返回0;如果创建失败,父进程返回-1,并且置上errno,没有子进程被创建;

回收子进程:wait

wait是一种系统调用,用于父进程探测子进程的状态变化。子进程退出的时候,内核还保留数据结构保存退出状态,当父进程调用wait,如果此时已经有子进程退出,那么立即返回,如果没有,父进程会阻塞在wait调用上,直到至少一个子进程退出(内核给父进程发送SIGCHILD信号),然后系统会把子进程的资源彻底释放。如果父进程没有子进程,那么会立即报错返回。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值: 成功返回子进程的ID号,失败返回-1,置上errno;

回收一个特定子进程:waitpid

waitpid用于等待特定的子进程,并且可以设置阻塞还是非阻塞。

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid:
	pid < -1: 那么等待进程组ID等于pid绝对值的进程组中的进程;
	pid = -1: 等待所有的子进程;
	pid = 0: 等待进程组ID等于父进程组ID的进程组中的进程;
	pid > 0: 等待进程ID等于pid的子进程
options:
	WNOHANG: 正常waitpid的时候父进程会hang住,用这个参数,如果没有子进程退出,立即返回0;
	...
status:输出型参数,如果status非NULL,那么会返回子进程的状态,通过一些宏可以获得状态;
	WIFEXITED(*status): 子进程正常退出,返回true;
	WEXITSTATUS(*status): 如果子进程正常退出的话,返回返回码;
	WIFSIGNALED(*status): 子进程被信号中断退出,返回true;
	WTERMSIG(*status): 中断子进程的信号ID;
	WCOREDUMP(*status): 子进程coredump了,返回true,同时WIFSIGNALED也返回true;
	...
返回值: 成功,返回子进程的ID号;失败,返回-1,置上errno;如果子进程ID不存在,或者存在但是非该进程的子进程,返回-1,并且置上errno;如果子进程还没结束,并且使用非阻塞模式,那么返回0.

执行新的程序

exec函数族用于执行一个新的程序代替当前程序。函数包括execl、execv、 execle、execve、execlp、execvp、execvp等,底层都是使用execve系统调用。这几个函数命名方式有一定规律,v表示向量,就是用二维指针来传递参数,l表示list,就是用多个指针传递参数,e表示传递环境变量,p表示寻找可执行文件的顺序和shell一样。

指定参数和环境变量:execve

execve系统调用用于执行filename指向的可执行程序或者shell脚本,脚本可以被执行有两个条件:

  1. 脚本具有可执行权限
  2. 脚本开头必须指定解释器,比如:#!/bin/bash,否则会报 Exec format error

执行execve之后,当前程序的代码段、数据段、bss段、栈等信息会被filename指向的程序覆盖,因此没有返回值同时被执行程序的进程ID和当前程序的ID相同。

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
filename: 被执行的程序;
argv: 传递给被执行程序的参数,以NULL结尾;
envp: 传递给被执行程序的环境变量(key=value的格式),以NULL结尾;
如果main函数的定义为:int main(int argc, char *argv[], char *envp[]);那么可以argv和envp就指向execve中的argv和envp,注意,如果filename=/usr/bin/ls, argv={"-l", NULL},那么ls执行的时候argv[0]="-l",这个和直接执行ls -l不同,可能会影响到传参的解析,所以最好argv={"ls", "-l", NULL};
返回值: 成功不返回,失败返回-1,并且置上errno。

以列表方式传参:execl

execl函数使用列表方式传参,最后一个参数之后以NULL结尾,就是每个参数使用一个指针。不用传递环境变量,默认使用当前程序的环境变量。

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path: 被执行程序;
arg: 指向单个参数的指针;
返回值:同execve;

代码示例:

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

int
main(int argc, char **argv)
{
        if (argc != 2){
                printf("usage: %s filename\n", argv[0]);
                return 0;
        }
        char a[] = "hello";
        char b[] = "world";
        execl(argv[1], a, b, NULL);
        return 0;
}

以向量方式传参:execv

execv函数使用向量方式传参,默认使用当前程序的环境变量。

#include <unistd.h>
int execv(const char *path, char *const argv[]);
path: 被执行程序;
argv: 指向参数的二维指针;
返回值:同execve;

以列表方式传参并传递环境变量:execle

#include <unistd.h>
int execle(const char *path, const char *arg, ..., char * const envp[]);
path: 被执行程序;
arg: 指向单个参数的指针;
envp: 指向环境变量的指针;
返回值:同execve;

代码示例

#include <unistd.h>
#include <stdio.h>
int
main(int argc, char **argv)
{
        if (argc != 2){
                printf("usage: %s filename\n", argv[0]);
                return 0;
        }
        char a[] = "hello";
        char b[] = "world";
        char *c[] = {"name=xiaoming", "age=18", NULL};
        int ret;
        ret = execle(argv[1], a, b, NULL, c);
        if (ret == -1) {
                perror("execle: ");
        }
        return 0;
}

特定执行顺序:execlp、execvp、execvpe

execlp、execvp、execvpe函数的功能分别和execl、execv、execve相同,不同点在于:

  1. 参考shell寻找可执行文件的逻辑
  2. 如果不是绝对路径,先从PATH环境变量中找,找不到就从当前目录下寻找。
  3. 如果PATH路径下程序找到了,但是没有可执行权限,那么继续从下一级目录下寻找
  4. 如果被执行的程序是shell脚本,但是没有解释器,比如:#!/bin/bash,那么会默认使用/bin/sh来解释

简单执行shell命令:system

system库函数用于执行一个shell命令,也可以是一个可执行程序,不需要传参,被执行程序的环境变量和调用程序相同。system的底层逻辑为:fork一个子进程----子进程exec一个shell—父进程waitpid子进程执行结束—返回status

#include <stdlib.h>
int system(const char *command);
command: 被执行的程序;
返回值: 如果创建子进程失败,返回-1;如果sh没有被执行,比如权限不足,返回127;如果执行成功,返回wait(*status)中的status,通过WEXITSTATUS(status)获取command命令的返回码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值