C语言笔记-18-Linux基础-进程
文章目录
前言
自学笔记,没有历史知识铺垫(省略百度部分)C语言笔记-18-Linux基础-进程
一、进程概括
- 程序是计算机指令集合,静态数据
- 进程是程序在计算机运行的实例
- 程序在运行过程中,要使用计算机资源,就要对进程使用的资源进行记录,所有的记录约等于进程
- 进程是资源分配的基本单位,每个进程都有自己的pid和PCB
注意:
- 进程有独立的4G地址空间(多个进程轮流布局同一份4G地址空间,同一时刻只有一个进程独立占用)
- 进程有自己的pid
- 进程有自己的PCB
二、进程指令
- ps 查看进程
- top 实时查看进程
PCB成员
fd 记录进程对文件资源的使用情况(文件描述符)
image 进程镜像(轮流布局独立4G地址空间映射)
三、进程函数
unistd.h
fork 新建子进程
pid_t fork(void);
新建子进程(新建失败时,不会创建子进程)
返回值: 成功时,在父进程和子进程中各返回一次(常说的一次调用,两次返回)
- 成功:pid 子进程的pid 返回到父进程中
- 成功:0 返回到子进程中
- 失败:-1 返回到父进程中,errno被设置
注意:
- 子进程的PCB和父进程的PCB读的部分一致,写的部分,子进程会复制父进程PCB该部分后单独存储(2.6版本后增加,写时拷贝技术),即image布局是一样,但是子进程与父进程各运行在独立的内存空间,二者操作本进程时,不会相互影响
可以通过fork返回标识,控制业务在父进程or子进程中处理(异步)
示例代码
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t pid = fork();
if (pid == -1)
{
printf("父进程执行失败,该信息永远在父进程中输出\n");
}
if (pid == 0)
{
printf("子进程pid才会返回0,该信息永远是在子进程中输出\n");
}
else
{
printf("父进程才会返回子进程的pid:%d,该信息永远在父进程中输出\n", pid);
}
printf("由于子进程复制父进程的PCB,该信息会在父进程和子进程都会输出\n");
return 0;
}
// 输出结果
父进程才会返回子进程的pid:7724,该信息永远在父进程中输出
由于子进程复制父进程的PCB,该信息会在父进程和子进程都会输出
子进程pid才会返回0,该信息永远是在子进程中输出
由于子进程复制父进程的PCB,该信息会在父进程和子进程都会输出
return/exit 关闭子进程
return
标识函数结束,在main函数中,终止进程
exit
终止进程 (stdio操作会被刷新关闭,创建的临时文件也会被移除) 并向父进程返回(status&0377)
(0-255)
atexit 遗言函数
进程在正常终止时被调用的函数,如:main函数return后、进程exit后,才会调用的函数
注册遗言函数
atexit()
传入无行参的指针函数
- 执行顺序按照注册顺序倒叙执行
- 同一个函数可以被注册多次,执行多次
- 子进程继承父进程的遗言函数
exec
家族中的函数,只要有一个函数调用成功,其他遗言函数会被移除(不执行)
#include <stdio.h>
#include <stdlib.h>
void endCall(){
printf("遗言函数触发\n");
}
int main(int argc, char *argv[])
{
atexit(endCall);
atexit(endCall);
return 0;
}
// 输出结果
遗言函数触发
遗言函数触发
注册遗言函数
on_exit()
传入有行参的指针函数,并传入返回值,功能与atexit()
一样
四、wait 进程资源回收
进程终止的时候,需要将进程资源回收,终止的进程无法自己回收,回收操作由父进程进行。
回收函数wait
家族函数回收
wait
阻塞并挂起当前进程,等待任意子进程终止。
wait(int *status)
入参:
*status 将子进程状态信息存储到该指针中
返回值:
成功: pid 返回终止了的子进程的pid
失败: -1 errno被设置
wait
的宏
- WIFEXITED(status) 子进程正常终止,返回真
- WEXITSTATUS(status) 返回子进程的退出状态码(exit(status) status&0377)
这个宏只能在WIFEXITED返回真时使用
- WIFSIGNALED(status) 如果子进程是被信号终止的,返回真
- WTERMSIG(status) 返回导致子进程终止的信号的编号
这个宏只能在WIFSIGNALED返回真时使用
wait可以让异步执行的多进程,转变为同步执行
模拟子进程正常终止示例
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int subExitStatus;
pid_t pid = fork();
if (pid == -1)
{
printf("父进程执行失败,该信息永远在父进程中输出\n");
}
if (pid == 0)
{
printf("子进程pid才会返回0,该信息永远是在子进程中输出\n");
return 99;
}
else
{
wait(&subExitStatus);
if (WIFEXITED(subExitStatus))
{
printf("子进程正常终止状态码:%d\n", WEXITSTATUS(subExitStatus));
}
printf("父进程才会返回子进程的pid:%d,该信息永远在父进程中输出\n", pid);
}
printf("由于子进程被提前return,该信息永远在父进程中输出\n");
return 0;
}
// 执行结果
子进程pid才会返回0,该信息永远是在子进程中输出
子进程正常终止状态码:99
父进程才会返回子进程的pid:12483,该信息永远在父进程中输出
由于子进程被提前return,该信息永远在父进程中输出
模拟子进程信号打断示例
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int subExitStatus;
pid_t pid = fork();
if (pid == -1)
{
printf("父进程执行失败,该信息永远在父进程中输出\n");
}
if (pid == 0)
{
printf("子进程pid才会返回0,该信息永远是在子进程中输出,pid:%d\n", getpid());
getchar();
return 99;
}
else
{
wait(&subExitStatus);
if (WIFEXITED(subExitStatus))
{
printf("子进程正常终止状态码:%d\n", WEXITSTATUS(subExitStatus));
}
if (WIFSIGNALED(subExitStatus))
{
printf("子进程信号打断状态码:%d\n", WTERMSIG(subExitStatus));
}
printf("父进程才会返回子进程的pid:%d,该信息永远在父进程中输出\n", pid);
}
printf("由于子进程提前被信号终打断,该信息永远在父进程中输出\n");
return 0;
}
// 执行结果
子进程pid才会返回0,该信息永远是在子进程中输出,pid:13385
子进程信号打断状态码:8
父进程才会返回子进程的pid:13385,该信息永远在父进程中输出
由于子进程提前被信号终打断,该信息永远在父进程中输出
打断信号
dony15$ kill -8 13385
waitpid
阻塞并挂起当前进程,等待指定子进程终止。
pid_t waitpid(pid_t pid, int *stat_loc, int options);
pid
指定具体的子进程
<-1
等待进程组的pid是|pid|的组中的子进程-1
等待任意子进程0
等待和当前进程同一个进程组的子进程>0
指定了要等待的子进程的pidstat_loc 同
wait(int *status)
的参数
options
WNOHANG
如果没有子进程终止,立即返回0
阻塞等待子进程的终止
返回值:
成功 返回状态改变的子进程的pid
如果WNOHANG被指定,没有子进程状态的改变 返回0
错误 -1 errno被设置
// 效果
wait(s)==waitpid(-1,s,0)
五、exec家族函数 进程映像的更新
子进程可以更新image,此时子进程的image与父进程的image将不同(更新子进程image不影响父进程)
execve
exec核心函数,所有exec家族函数都是围绕这个函数封装
int execve(const char *path, char *const argv[], char *const envp[]);
功能:执行程序
参数:
- filename 要执行的文件名
这个文件必须是可执行的二进制文件或脚本文件- argv 字符串数组 传递给main函数的参数 习惯上第一个是可执行文件的名字
- envp name=value 作为环境变量传递给新的可执行程序
argv和envp以NULL作为结尾
返回值:
成功 不返回
错误 -1 errno被设置
exec家族函数列表
#include <unistd.h>
//指向当前进程的环境变量列表
extern char **environ;
/* (char *) NULL */);
int execl(const char *path, const char *arg, ...
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[]);
exec 公有的
l
l代表list,需要将arg元素以可变参数都传入
v
v代表vector,传数组即可
p
PATH环境变量 需要知道要加载的可执行程序所在的路径.
- 有p字母,会到环境变量PATH指定的路径中找可执行程序
- 没有p字母,必须指定可执行文件的所在路径(相对 绝对)
e
- 如果有字母e,用户指定环境变量传递给新的可执行程序.
- 没有字母e.代表从父进程继承环境变量
execvp示例
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t pid = fork();
if (pid == -1)
{
printf("父进程执行失败,该信息永远在父进程中输出\n");
}
if (pid == 0)
{
char *ps_argv[] = {"ps", "-o", "pid,ppid,comm", NULL};
execvp("ps", ps_argv);
printf("execvp执行失败才会执行\n");
}
else
{
printf("父进程才会返回子进程的pid:%d,该信息永远在父进程中输出\n", pid);
}
printf("由于子进程镜像被更新(execvp执行失败,子程序才会执行),与父进程的PCB不同,该信息会在父进程输出\n");
return 0;
}
// 执行结果
父进程才会返回子进程的pid:14966,该信息永远在父进程中输出
由于子进程镜像被更新(execvp执行失败,子程序才会执行),与父进程的PCB不同,该信息会在父进程输出
PID PPID COMM
999 998 -bash
5836 5805 /bin/bash
10801 10800 -bash
10839 10801 vim
13388 5805 /bin/bash
execlp示例
...
execlp("ps", "ps", "-o", "pid,ppid,comm", NULL);
...
// 执行结果
父进程才会返回子进程的pid:14966,该信息永远在父进程中输出
由于子进程镜像被更新(execvp执行失败,子程序才会执行),与父进程的PCB不同,该信息会在父进程输出
PID PPID COMM
999 998 -bash
5836 5805 /bin/bash
10801 10800 -bash
10839 10801 vim
13388 5805 /bin/bash
查看当前进程环境变量示例
#include <unistd.h>
#include <string.h>
#include <stdio.h>
// 当前进程的环境变量列表
extern char **environ;
int main(int argc, char *argv[])
{
for (size_t i = 0; environ[i] != NULL; i++)
{
printf("环境变量:%s\n", environ[i]);
}
return 0;
}
// 执行结果
环境变量:SHELL=/bin/bash
环境变量:TMPDIR=/var/folders/3j/nltc60p50b9dl803pp8qkbqc0000gn/T/
环境变量:OLDPWD=/Users/dony15/Desktop/c_code/c_learn_3
环境变量:ORIGINAL_XDG_CURRENT_DESKTOP=undefined
环境变量:MallocNanoZone=0
环境变量:JAVA_11_HOME=/Library/Java/JavaVirtualMachines/jdk-11.0.4.jdk/Contents/Home
环境变量:LC_ALL=en_US.UTF-8
环境变量:NO_PROXY=127.0.0.1
环境变量:COCOS_CONSOLE_ROOT=/Users/dony15/Desktop/cocos2d-x-4.0/tools/cocos2d-console/bin
环境变量:USER=dony15
环境变量:COCOS_TEMPLATES_ROOT=/Users/dony15/Desktop/cocos2d-x-4.0/templates
环境变量:COMMAND_MODE=unix2003
环境变量:JAVA_12_HOME=/Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home
环境变量:SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.cT3LzZpnc1/Listeners
环境变量:__CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34
...
execle示例
main.c
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
char *m_env[] = {"name=abc", "age=15",NULL};
int a = execle("./t_child.out", "abc", NULL, m_env);
perror("err:");
return 0;
}
t_child.c
#include <unistd.h>
#include <string.h>
#include <stdio.h>
// 当前进程的环境变量列表
extern char **environ;
int main(int argc, char *argv[])
{
printf("start t_child process pid:%d\n", getpid());
for (size_t i = 0; argv[i] != NULL; i++)
{
printf("传入变量:%s\n", argv[i]);
}
for (size_t i = 0; environ[i] != NULL; i++)
{
printf("环境变量:%s\n", environ[i]);
}
printf("t_child结束\n");
return 0;
}
dony15$ gcc -o t_child.out t_child.c
dony15$ gcc -o main main.c
dony15$ ./main
start t_child process pid:9380
传入变量:abc
环境变量:name=abc
环境变量:age=15
t_child结束
六、Linux ELF 可执行文件
elf格式为可执行文件,python、bash、自定义写的c.out等都是
readelf 查看elf信息指令
readelf -a main.out
总结
注意:
char *argv[] 中argv是常量,不可以argv++
char **environ; 中environ是变量,可以environ++
向量表:地址空间连续的数组
列表:地址空间可能不连续的数组
- 僵尸进程:进程已经终止,进程资源没有回收
- 孤儿进程:父进程已终止,子进程没有回收,这种子进程需要过继给另一个进程。
1.ubuntu 15.04版本之前,过继给init进程(孤儿院)
2.ubuntu 15.04版本之后,过继给upstart进程(孤儿院)
exec家族函数,无论是环境变量列表、数组,还是入参变量列表、数组,结尾必须要加
NULL
本章主要为C语言笔记-18-Linux基础-进程