进程
1. 进程与程序
1.1 main函数由谁调用
- 执行main之前需要执行一段引导代码,由链接器完成,一起构成可执行文件
- 程序运行需要通过操作系统的加载器来实现,加载器是操作系统中的程序,当执行程序时,加载器负责将此应用程序加载内存中去执行
- 当在终端执行程序时,命令行参数(command-line argument)由 shell 进程逐一进行解析,shell 进程会将这些参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,当引导程序调用 main()函数时,在由它最终传递给 main()函数,如此一来,在我们的应用程序当中便可以获取到命令行参数了(argc,argv传参原理)
1.2 程序如何结束
程序结束就是进程终止,进程终止有多种分为正常终止和异常终止。
正常终止包括:main函数调用return,exit函数终止,_Exit()和_exit()终止进程
异常终止包括:调用abort函数,进程接收到信号SIGKILL信号
注册进程终止处理函数atexit():
#include <stdlib.h> int atexit(void (*function)(void));
function函数指针,无传入参数,无返回值,指向注册函数
返回值:成功返回0,失败返回-1!注意:_exit和_Exit函数不会执行atexit注册的终止处理函数
1.3 何为进程
进程其实就是一个可执行程序的实例,这句话如何理解呢?可执行程序就是一个可执行文件,文件是一个静态的概念,存放磁盘中,如果可执行文件没有被运行,那它将不会产生什么作用,当它被运行之后,它将会对系统环境产生一定的影响,所以可执行程序的实例就是可执行文件被运行。进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期.
1.4 进程号
概念:Linux系统下每一个进程都有一个进程号(PID),进程号是一个整数,用于唯一标识系统中的某一个进程。ps -aux查看系统下进程的相关信息
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);//获取该进程的进程号
pid_t getppid(void);//获取父进程的进程号
用法:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid(); //获取本进程 pid
printf("本进程的 PID 为: %d\n", pid);
pid = getppid(); //获取父进程 pid
printf("父进程的 PID 为: %d\n", pid);
exit(0);
}
2. 进程的环境变量
shell终端下使用env命令查看shell进程的环境变量,是以字符串数组列表格式存储,每一项都以“名称=值”(name=vlaue)格式.
使用export命令可以添加或者删除一个环境变量export LINUX_APP=123456 # 添加 LINUX_APP 环境变量 export -n LINUX_APP # 删除 LINUX_APP 环境变量
2.1 应用程序中获取环境变量
进程的环境变量会继承父进程环境变量,譬如shell终端下执行应用程序,这个进程的环境变量就是从父进程shell中继承而来的.
- 在应用程序中通过environ变量指向环境变量的字符串数组
只要程序中声明了environ全局变量,就可以调用来打印环境变量extern char **environ; // 申明外部全局变量 environ
for(int i =0;NULL!=environ[i];i++) { puts(environ[i]); }
- 只想获取指定环境变量 getenv()
#include <stdlib.h> char *getenv(const char *name);//name 要找的环境变量名称,返回指向该值的指针,失败返回NULL
2.2 添加/删除/修改环境变量
- putenv函数
!注意:putenv将直接改变environ变量里的某个元素,所以不能随意修改string指向的内容#include <stdlib.h> int putenv(char *string);//string 是一个字符串指针,指向name=value的字符串;成功返回0失败返回非零并且设置errno
- setenv函数
可以替代putenv用于添加或修改环境变量#include <stdlib.h> int setenv(const char *name, const char *value, int overwrite); //name环境变量名称 //value环境变量值 //overwrite若参数 name 标识的环境变量已经存在,在参数 overwrite 为 0 //的情况下,setenv()函数将不改变现有环境变量的值,也就是说本次调用没有 //产生任何影响;如果参数 overwrite 的值为非 0,若参数 name标识的环境 //变量已经存在,则覆盖,不存在则表示添加新的环境变量。
- unsetenv函数
从环境变量表中移除参数 name 标识的环境变量#include <stdlib.h> int unsetenv(const char *name);
2.3 清空环境变量
- environ = NULL;
- clearenv函数
#include <stdlib.h> int clearenv(void);
2.4 环境变量的作用
环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。
3. 进程的内存布局(?)
4. 进程的虚拟地址
Linux 系统下,应用程序运行在一个虚拟地址空间中,所以程序中读写的内存地址对应也是虚拟地址,并不是真正的物理地址,譬如应用程序中读写 0x80800000 这个地址,实际上并不对应于硬件的 0x80800000这个物理地址。
为什么需要引入虚拟地址?
①多个程序运行时,必须保证这些程序用到的内存总量小于计算机实际的物理内存的大小
②内存使用效率低
③进程地址空间不隔离
④无法确定程序的链接地址
虚拟地址优势:
5. fork创建子进程
函数原型:
#include <unistd.h>
pid_t fork(void);
优势:创建多个进程是任务分解时行之有效的方法,譬如网络服务器需要监听客户需求的同时,为每一个请求时间进行处理而创建许多子进程
缺点:fork可以看做对父进程的数据段、堆段、以及其他一些数据结构创建拷贝,复制父进程绝大多数数据段和堆段效率降低,浪费。fork第二使用场景就是子进程会调用exec函数,执行新的代码,不需要父进程的数据段、堆段、栈段中的数据,导致浪费时间。不过现在内核技术高级出现写时复制技术很大程度减少了时间浪费
fork函数关键在于,完成对其调用后将存在两个进程,子进程和父进程在各自的运行空间中进行。
注意父进程中PID返回的是子进程的pid,子进程中pid返回是0,子进程是父进程的完全拷贝,但共享代码段,之占用一份代码段内存
测试1:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
pid_t pid;
pid = fork();
switch (pid) {
case -1:
perror("fork error\n");
exit(-1);
case 0:
printf("这是子进程的pid:%d,父进程的pid:%d\n",getpid(),getppid());
_exit(0);
default:
printf("这是父进程的pid:%d,子进程的pid:%d\n",getpid(),pid);
exit(0);
}
exit(0);
}
6. 父、子进程间的文件共享
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd;
pid_t pid;
int i;
fd = open("./test.txt", O_RDWR | O_CREAT | O_EXCL,0666);
if(fd== -1)
{
perror("open error\n");
exit(-1);
}
pid = fork();
switch (pid)
{
case -1:
perror("fork error\n");
exit(-1);
case 0:
for(i=0;i<4;i++)
{
if(write(fd, "AABBCC", 6)==-1)
{
perror("write error\n");
_exit(-1);
}
}
_exit(0);
default:
for(i=0;i<4;i++)
{
if(write(fd, "112233", 6)==-1)
{
perror("write error\n");
exit(-1);
}
}
exit(0);
}
}
fork应用场景
7. 系统调用 vfork()
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork()可以为调用该函数的进程创建一个新的子进程,然而,vfork()是为子进程立即执行 exec()新的程序而专门设计的,也就是 fork()函数的第二个使用场景
## 8. fork()之后的竞争条件
创建子进程后并不确定父子谁先运行,利用阻塞机制显然父进程阻塞等待子进程发送信号
#include <bits/types/sigset_t.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void handler()
{
printf("recieve signal\n");
}
int main(){
struct sigaction sig = {0};
sigset_t wait_sig;
sigemptyset(&wait_sig);
sig.sa_handler = handler;
sig.sa_flags = 0;
if(sigaction(SIGUSR1, &sig, NULL)==-1){
perror("sigaction error\n");
exit(-1);
}
switch (fork()) {
case -1:
perror("fork error\n");
exit(-1);
case 0:
printf("子进程执行\n");
printf("~~~~~~~~~~~~~~~\n");
sleep(2);
kill(getppid(), SIGUSR1);
_exit(0);
default:
if(-1 != sigsuspend(&wait_sig)){
perror("sigsuspend error\n");
exit(-1);
}
printf("父进程开始执行 \n");
printf("fff\n");
exit(0);
}
}
9. 进程的诞生与终止
9.1 进程的诞生
所有的进程都是由其父进程创建的
pid=1的进程就是所有进程的父进程(init进程),由内核创建而来,一切从 1 开始、一切从 init 进程开始
9.2 进程的终止(?)
进程终止方式:正常终止和异常终止
一般使用 exit()库函数而非_exit()系统调用,原因在于 exit()最终也会通过_exit()终止进程,但在此之前,它将会完成一些其它的工作,exit()函数会执行的动作如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("Hello World!\n");
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
exit(0);
default:
/* 父进程 */
exit(0);
}
}
/*只打印一行的原因,标准输出加换行符会立即读走,fork创建子进程后复制的缓冲区已经是空的,所以exit后没有打印*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("Hello World!");
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
exit(0);
default:
/* 父进程 */
exit(0);
}
}
/*不加换行符会一直存在缓冲区等待刷新,创建子进程后复制了带有内容的缓冲区,所以,父进程exit时打印一次,子进程exit时打印一次*/
10. 监视子进程
父进程需要知道子进程何时被终止以及终止时的一些状态信息,是正常终止还是异常终止还是被信号终止。
10.1 wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
/*status:参数 status 用于存放子进程终止时的状态信息,
参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。*/
调用wait函数的过程:
①:如果所有子进程都还在运行,那么wait会阻塞等待
②:如果没有子进程没有运行的,那么不会等待并且返回-1,设置erron
③:如果调用wait时已经有一个或者多个子进程终止,那么wait也不会阻塞等待,因为wait有收尸功能,处理后事
10.2 waitpid()函数
wait函数的缺陷:
①:无法等待特定的子进程,一个一个来
②:如果子进程没有终止,wait只能保持阻塞等待,不能执行非阻塞等待
③:使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
/*status:与 wait()函数的 status 参数意义相同
pid:
⚫ 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
⚫ 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
⚫ 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
⚫ 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍。
参数 options 是一个位掩码,可以包括 0 个或多个如下标志:
⚫ WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等
待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没
有发生改变。
⚫ WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进
程状态信息;
⚫ WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(){
int ret;
int i;
int status;
for(i=1;i<=3;i++){
switch(fork()){
case -1:
perror("fork error\n");
exit(-1);
case 0:
printf("子进程<%d>被创建\n",getpid());
sleep(i);
_exit(i);
default:
break;
}
}
sleep(1);
printf("~~~~~~~~~~~~~~~~~~~~~\n");
for(;;){
ret = waitpid(-1,&status,WNOHANG);
if(ret < 0)
{
if(errno == ECHILD)
{
exit(0);
}
else{
perror("waitpid error\n");
exit(-1);
}
}
else if(ret == 0)
continue;
else
{
printf("释放子进程pid:%d,状态信息:%d\n",ret,WEXITSTATUS(status));
}
}
exit(0);
}
10.3 waitid()函数
10.4 僵尸进程与孤儿进程
根据父进程和子进程生命周期不同,出现了僵尸进程和孤儿进程
-
孤儿进程(父进程先于子进程终止)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ switch(fork()){ case -1: perror("fork error\n"); exit(-1); case 0: printf("子进程pid:%d,父进程pid:%d\n",getpid(),getppid()); sleep(3); printf("等待父进程终止\n"); printf("子进程pid:%d,父进程pid:%d\n",getpid(),getppid()); _exit(0); default: break; } sleep(1); printf("father over!\n"); exit(0); }
-
僵尸进程(子进程先于父进程终止)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { /* 创建子进程 */ switch (fork()) { case -1: perror("fork error"); exit(-1); case 0: /* 子进程 */ printf("子进程<%d>被创建\n", getpid()); sleep(1); printf("子进程结束\n"); _exit(0); default: /* 父进程 */ break; } for ( ; ; ) sleep(1); exit(0); }
10.5 SIGCHLD 信号
有两种情况会出现SIGCHLD信号:
①:当父进程某个子进程终止时,父进程会收到此信号
②:当父进程的某个子进程收到停止或者恢复信号时,内核有可能会发送此信号给父进程可以借用这一信号,创建信号处理函数wait来回收僵尸进程,但是有一个问题,在处理信号时如果又有相同信号发送过来,那么只能处理一次,会有漏网之鱼。
解决办法:
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
11. 执行新程序
利用exec函数来实现让子进程执行新的程序,不执行父进程的程序
11.1 execve函数
系统调用 execve()可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
/*
filename:路径名,可以是绝对路径也可以是相对路径
argv[],类似main(int argc,char *argv[]),arg[0]就是新程序文件路径
envp用来指定环境变量,格式name=value
*/
测试程序:并不是实际应用场景,一般用在fork子进程中执行新程序
虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗,所以就出现了 exec 操作
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]){
char *arg_ary[5];
char *env_arry[6]={"name=xiaoming","sex=man","age=18",NULL};
if(argc<2)
exit(-1);
arg_ary[0]=argv[1];
arg_ary[1]="Hello";
arg_ary[2]="world";
arg_ary[3]=NULL;
execve(argv[1], arg_ary , env_arry);
perror("execve error");
exit(-1);
}
创建新程序:
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[]){
char **ep = NULL;
for(int i = 0; i < argc; i++){
printf("argv[%d]=%s\n", i, argv[i]);
}
puts("env");
for(ep = environ; *ep != NULL;ep++)
{
printf("%s\n",*ep);
}
return 0;
}
11.2 exec库函数
-
execl()和 execv()
// execv 传参 char *arg_arr[5]; arg_arr[0] = "./newApp"; arg_arr[1] = "Hello"; arg_arr[2] = "World"; arg_arr[3] = NULL; execv("./newApp", arg_arr); // execl 传参 execl("./newApp", "./newApp", "Hello", "World", NULL);
-
execlp()和 execvp()
// execvpe 传参 char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL}; char *arg_arr[5]; arg_arr[0] = "./newApp"; arg_arr[1] = "Hello"; arg_arr[2] = "World"; arg_arr[3] = NULL; execvpe("./newApp", arg_arr, env_arr); // execle 传参 execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
-
exev 函数使用示例
//运行ls #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { execl("/bin/ls", "ls", "-a", "-l", NULL); perror("execl error"); exit(-1); }
11.3 system()函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令
#include <stdlib.h>
int system(const char *command);
/*
command:参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等。
返回值:关于 system()函数的返回值有多种不同的情况,稍后给大家介绍。
*/
12. 进程状态与进程关系
12.1 进程状态
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态
11.3 system()函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令
#include <stdlib.h>
int system(const char *command);
/*
command:参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等。
返回值:关于 system()函数的返回值有多种不同的情况,稍后给大家介绍。
*/
12. 进程状态与进程关系
12.1 进程状态
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态
12.2 进程关系
1、无关系
2、父子进程关系
3、进程组(方便对进程进行管理)[外链图片转存中…(img-nMAY3kpm-1720339855881)]
3、会话