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

概述

1、孤儿进程和僵尸进程

  • 僵尸进程

一个进程退出后内核会回收进程的资源但是会留下一个僵尸进程的数据结构保留了进程的ID、进程的状态等信息这些信息被父进程通过wait函数获取后被释放如果一个子进程退出后父进程没有wait这些信息那么这个子进程就变成了僵尸进程。

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

  1. 父进程通过wait收集子进程的退出信息
  2. 子进程退出时内核会向父进程发送SIGCHILD父进程处理该信号的时候通过wait获取退出信息
  3. 让进程被进程1接管进程1会wait每一个退出的子进程
  • 孤儿进程

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

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

进程终止

进程的编译和启动

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

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

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

argc和argv的传参:hell下执行进程的时候这俩参数首先被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

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

#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还可以继续用数据也不会被修改;

子进程

创建子进程:fork

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

  • 子进程除了代码段数据、堆、栈都复制了一份副本父子进程对数据的访问互相不影响
  • 子进程复制父进程文件描述符但是指向同一个文件表项共享文件偏移量
  • 父子进程返回值不同
  • 父子进程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调用上直到至少一个子进程退出然后系统会把子进程的资源彻底释放。

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

探测特定一个子进程状态变化: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.

执行另一个程序

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;
返回值: 成功不返回失败返回-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来解释
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6