文章目录
- 概述
- 1、孤儿进程和僵尸进程
- 进程终止
- 进程的编译和启动
- 进程终止的步骤
- 进程8种终止方式
- 进程退出函数1:exit
- 进程退出函数2:_exit
- 进程退出函数3:_Exit
- 注册终止处理程序:atexit
- 环境变量
- 通过main函数传参
- 全局的环境变量表:environ
- 获取环境变量:getenv
- 修改环境变量:putenv
- 修改环境变量:setenv
- 进程堆空间申请和释放
- 申请指定大小的内存:malloc
- 申请初始化的内存:calloc
- 修改已申请内存大小:realloc
- 子进程
- 创建子进程:fork
- 创建子进程:vfork
- 探测子进程状态变化:wait
- 探测特定一个子进程状态变化:waitpid
- 执行另一个程序
- 指定参数和环境变量:execve
- 以列表方式传参:execl
- 以向量方式传参:execv
- 以列表方式传参并传递环境变量:execle
- 特定执行顺序:execlp、execvp、execvpe
概述
1、孤儿进程和僵尸进程
- 僵尸进程
一个进程退出后,内核会回收进程的资源,但是会留下一个僵尸进程的数据结构,保留了进程的ID、进程的状态等信息,这些信息被父进程通过wait函数获取后被释放,如果一个子进程退出后,父进程没有wait这些信息,那么这个子进程就变成了僵尸进程。
防止进程变成僵尸进程的几种方式:
- 父进程通过wait收集子进程的退出信息
- 子进程退出时,内核会向父进程发送SIGCHILD,父进程处理该信号的时候通过wait获取退出信息
- 让进程被进程1接管,进程1会wait每一个退出的子进程
- 孤儿进程
父进程退出后,子进程还没有退出,那么子进程就会被进程1接管变成孤儿进程。因此子进程可以通过下面的命令来判断父进程是否退出了:
while(getppid() != 1) sleep(1);
进程终止
进程的编译和启动
进程的编译链接:裸机编程需要写链接脚本,但是linux下编程就不用了,因为每个进程的链接方式都是固定的,gcc会自动把事先准备好的引导代码链接到main函数的前面,这段代码每个程序都是一样的。
进程的加载:进程运行的时候,加载器会把进程加载到内存中,然后去执行。
进程编译的时候使用链接器,运行的时候使用加载器
argc和argv的传参:hell下执行进程的时候,这俩参数首先被shell解析,然后传递给加载器,最后通过main函数传递给进程,所以我们在main函数中能使用这两个参数。
进程终止的步骤
进程启动的时候,内核会通过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种终止方式
正常终止方式:
- 从main返回
- 调用exit
- 调用_exit或_Exit
- 最后一个线程 从其启动例程返回
- 最后一个线程调用thread_exit
异常终止:
- 调用abort
- 接到一个信号
- 最后一个线程对取消请求做出响应
进程退出函数1:exit
这是一种进程正常退出的库函数,通过man 3 exit可以查看,exit函数没有返回值,会返回status状态码给调用他的父进程。
进程调用exit时候,会执行以下步骤:
- 先调用终止处理程序。终止处理程序通过atexit函数注册,一个进程最多注册32个。调用的顺序和注册的顺序相反。终止处理程序没注册一次会被调用一次,尽管是相同的函数注册了多次
- 所有打开的标准IO流会被flush和close
- 通过tmpfile创建的临时文件会被删除
#include <stdlib.h>
void exit(int status);
status:给父进程的返回码
进程退出函数2:_exit
_exit是一个系统调用,作用也是退出程序,但是不会去执行终止处理函数、清理IO、删除临时文件等步骤,直接进入到内核:
- 所有的文件描述符会被直接关闭
- 然后所有的子进程被进程1接管
- 给父进程传递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
作用如下:
- 如果环境变量不存在则添加环境变量
- 如果环境变量已经存在,那么把环境变量的值设置成最新的值。
注意事项:
- putenv了之后,string地址会添加到environ表中。
- putenv了之后,如果修改了string的内容,那么环境变量也会对应被修改。
- 环境变量的修改只会影响当前进程和子进程的环境变量表,对父进程无效
#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设置修改后的内存大小,可以改大也可以改小,大体分为以下几个场景:
- 如果改小:那么修改前的内存的起始地址到size大小的范围内的数据不会被修改,返回指向修改前的内存指针
- 如果改大,将在原来堆地址继续往高地址空间扩展,有两种可能,1)一是空间连续且足够,那么返回原来的空间地址,注意新增加的内存不会被初始化;2)二是连续的空间不够,那么寻找新的地址空间,并将原来的数据转移到新的空间中,原来的内存会被自动释放掉,返回新的空间地址
ptr指针和size之间的组合关系大体分为以下几种情况:
- ptr等于NULL:realloc等效于malloc()
- ptr不等于NULL:ptr必须是通过malloc(), calloc(), realloc()分配过的
- 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之间的区别在于:
- 子进程和父进程之间共享内存数据,包括代码段、数据段、堆、栈等,创建子进程的效率比fork高
- 子进程先执行,父进程会卡主,直到子进程调用了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脚本,脚本可以被执行有两个条件:
- 脚本具有可执行权限
- 脚本开头必须指定解释器,比如:#!/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相同,不同点在于:
- 参考shell寻找可执行文件的逻辑
- 如果不是绝对路径,先从PATH环境变量中找,找不到就从当前目录下寻找。
- 如果PATH路径下程序找到了,但是没有可执行权限,那么继续从下一级目录下寻找
- 如果被执行的程序是shell脚本,但是没有解释器,比如:#!/bin/bash,那么会默认使用/bin/sh来解释