目录
1、进程关键概念
1.什么是程序,什么是进程,有什么区别
程序是静态的概念比如我们平时使用的qq、微信等都是程序,进程是程序的一次运行活动。一般来说我们的程序跑起来了,系统中就会相应的产生一个或多个进程
2.如何查看系统中有那些进程
1.使用ps指令查看
在实际应用中通常配合grep来查看程序中是否存在某一个进程
例:ps -aux|grep a 这里就会列举出含a的进程grep起到了过滤作用
2.使用top指令查看,top指令类似于Windows里的任务管理器
3.什么是进程标识符
每一个进程都有一个非负整数表示的唯一id,叫做pid,类似于我们的身份证
pid=0;称为交换进程(swapper)作用:进程调度
pid= 1;initial进程,作用:系统初始化
编程调用getpid函数获取自身的进程标识符,getppid是获取父进程的进程表示符
4.什么叫父进程,什么叫子进程
进程a创建了进程b,那么a进程就是父进程b进程就是子进程,父子进程是相对的概念
5.C语言的存储空间是如何分配的
-
代码区(Text Segment):
- 存储程序的机器代码,即编译后的可执行文件。
- 通常是只读的,防止程序意外修改自身的指令。
- 包含函数的二进制表示。
-
数据区(Data Segment):
- 存储全局变量、静态变量和常量。
- 包括已初始化数据区(Initialized Data Segment)和未初始化数据区(BSS段)。
- 已初始化数据区存储已经明确赋值的全局变量、静态变量和常量。
- 未初始化数据区存储未被明确赋值的全局变量和静态变量,这部分数据在程序运行前会被初始化为零。
-
堆区(Heap):
- 用于动态内存分配,例如通过
malloc
、calloc
、realloc
等函数分配的内存。 - 堆的大小和位置可以在运行时动态改变。
- 需要程序员手动管理分配和释放内存,否则可能导致内存泄漏或悬挂指针等问题。
- 用于动态内存分配,例如通过
-
栈区(Stack):
- 用于存储函数的局部变量、函数参数、返回地址和函数调用的上下文信息。
- 每个函数调用都会在栈上分配一块内存,该块在函数返回时会被释放。
- 栈是一个后进先出(LIFO)的数据结构,通过栈指针(Stack Pointer)来管理。
2、进程创建
父进程与子进程是交替运行的,谁先谁后是由进程调度决定的,在 fork
之后,父进程和子进程会共享相同的代码段、数据段和堆,但各自有独立的栈。当父进程或者子进程需要修改某一处时,系统就会为子进程分配相应的空间(写拷贝)
1.fork函数创建进程
pid_t fork(void);
返回值:
1.返回正数,当前运行的是父进程,返回值就是子进程的pid
2.返回负数,表示调用失败
3.返回0,当前运行的是子进程
2.vfork函数创建进程
pid_t vfork(void);
fork与vfork的区别
1.vfork 直接使用父进程存储空间,不拷贝。
2.vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
创建子进程的一般目的:一个父进程希望复制自己,使父子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,创建一个子进程来处理此请求,父进程则继续等待下一个服务请求到达。还有就是一个进程需要执行一个不同的程序时。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
// 获取当前进程的PID(进程ID)
int pid = getpid();
printf("pid = %d\n", pid);
// 创建子进程
int fork_pid = fork();
// 根据fork的返回值判断当前是父进程还是子进程
if (fork_pid > 0) {
// 父进程
while (1) {
// 打印父进程和子进程的PID
printf("pid = %d fork_pid = %d \n", pid, fork_pid);
sleep(3); // 休眠3秒
}
} else if (fork_pid == 0) {
// 子进程
while (1) {
// 打印子进程的PID(子进程内通过getpid获取)
// 注意:这里的fork_pid是0,因为在子进程中fork返回0
printf("pid = %d fork_pid = %d \n", getpid(), fork_pid);
sleep(3); // 休眠3秒
}
}
return 0;
}
3、进程退出
1.正常退出
1.main函数调用return
2.进程调用exit(),标准c库
3.进程调用_exit()或者_Exit(),属于系统调用
补充:进程最后一个线程返回,最后一个线程调用pthread_exit
exit()是对系统调用的封装,调用时会对进程运行产生的缓冲区的内容进行处理再退出,而_exit()是直接退出不处理
2.异常退出
1.调用abort函数使进程异常退出
2.当进程收到某些信号时,如ctrl+c
3.最后一个线程对取消(cancellation)请求做出响应
3.退出状态
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。一般来说我们都希望终止的进程能够通知父进程它是如何终止的。对于三个终止函数(exit、_exit、_Exit),实现这一点的方法是,将其退出状态作为参数传递给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能通过wait或waitpid函数取得其终止状态。
4、等待子进程
父进程等待子进程退出,并收集进程的退出状态
若是子进程退出状态不被收集,就会变成僵尸进程
1.wait函数
pid_t wait(int *status);阻塞等待
参数:
非空:子进程退出状态放在它所指的地址中
空:不关心退出状态
#include <stdio.h>
#include <stdlib.h>
int main()
{
int con = 0;
int pid1 = getpid();
int fork_pid = fork();
int pid2 = getpid();
int status;
if(pid1 == pid2){
wait(&status);
printf("%d\n",status);
printf("status = %d\n",WEXITSTATUS(status));
while(1){
printf("pid1 = %d , pid2 = %d , fork_pid = %d \n",pid1,pid2,fork_pid);
sleep(1);
}
}else if(pid1 != pid2){
while(1){
printf("pid1 = %d , pid2 = %d , fork_pid = %d \n",pid1,pid2,fork_pid);
con++;
sleep(1);
if(con == 5){
exit(33);
}
}
}
return 0;
}
2.waitpid函数
pid_t waitpid(pid_t pid, int *status, int options);可以设置成不阻塞等待,但是还是会存在僵尸进程
参数一:
pid==-1 等待任一子进程。就这方面而言,waitpid与wait等效
pid>0等待其进程ID与pid相等的子进程
pid==0等待其组ID等于调用进程组id的任一子进程
pid<-1等待其组ID等于pid绝对值的任一子进程
3.孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程,Linux避免系统存在过多孤儿进程,init进程会收留孤儿进程,变成孤儿进程的父进程。
5、exec族函数
1.exec族函数的作用
我们在用fork函数创建子进程后,经常会在新进程中调用exec族函数去执行另外一个程序。当调用exec族函数时,该进程会完全替换为一个新的程序。但是由于exec族函数不会创建新的进程,所以进程的pid并没有改变
当调用 exec 函数时,当前进程的代码、数据和堆栈都被新程序的代码、数据和堆栈所替代。因此,exec 函数族在当前进程的上下文中加载新的程序,而不是创建一个全新的进程。这是 exec 函数族与 fork 函数(用于创建新进程)不同的地方。
2.exec族函数
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., 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族函数的函数执行成功后不会返回,调用失败时,会设置errno(失败码)并返回-1,然后从原程序的调用点接着往下执行
设置的errno(失败码)可以通过perror()函数来打印函数失败的原因
参数说明:
path:可执行文件的路径
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量
3.代码说明
execl的使用
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1){
int pid = fork();
if(pid == 0){
printf("子进程\n");
execl("./zombie01","./zombie01",NULL);
}else if(pid > 0){
sleep(1);
printf("父进程\n");
}else{
printf("fork no !!!\n");
perror("fork");
}
}
return 0;
}
与下面的execlp做对比
#include <stdio.h>
#include <unistd.h>
int main()
{
int dis = execl("/bin/ps","ps",NULL);
if(dis == -1){
printf("no no no !!!\n");
perror("why");
}
}
execlp的使用
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("exec init \n");
if(execlp("ps","ps",NULL)==-1){
printf("exec = -1\n");
}
return 0;
}
execv的使用
#include <stdio.h>
#include <unistd.h>
int main()
{
char *argv[] = {"ls","-l"};
if(execv("/bin/ls",argv)==-1){
printf("execv no\n");
perror("execv");
}
return 0;
}
4.补充说明
1.perror函数
perror
函数是一个 C 标准库中的函数,用于将与当前errno
值相关联的错误消息输出到标准错误流(stderr)函数原型
void perror(const char *s);
s
: 一个字符串,用于在输出错误消息之前打印。通常是程序名或者与错误相关的描述性信息。
perror
会根据errno
的值输出相应的错误消息。通常在执行系统调用或库函数失败时,errno
会被设置为指示错误的特定代码。
2.whereis命令
whereis命令行工具
用于定位程序的可执行文件、源代码和帮助文档
选项:
-b
: 查找可执行文件。-m
: 查找帮助手册。-s
: 指定路径前缀例如whereis ls
将返回与
ls
相关的信息,包括可执行文件、源代码文件和帮助文档的路径。注意,
whereis
通常用于系统管理员或高级用户,它在不同的系统中可能有不同的行为。在某些系统上,它可能无法找到所有文件,因为它依赖于系统数据库。另外,如果你只想查找可执行文件的位置,你可能更喜欢使用which
命令
3.pwd命令
显示当前工作目录的路径
4.date命令
获取当前系统时间
5.echo $PATH
输出系统中设置的 PATH
环境变量的值
6.export PATH=$PATH:......
修改环境变量PATH里的值,$PATH只是表示原先的环境变量,路径之间用':'号隔开后面是需要添加的环境变量
6、system函数
1.system函数的作用
system函数,用于在一个新的进程中执行命令。这个函数会调用系统的 shell 来执行指定的命令。system函数会自己创建一个进程来执行可执行文件或命令,当运行完之后再运行父进程
大致的流程如下:
system
函数调用fork
来创建一个新的子进程。- 在子进程中,调用
exec
函数来执行指定的命令。- 父进程等待子进程的完成,并获取子进程的退出状态。
函数原型
int system(const char *command);
参数command: 要执行的命令字符串返回值
- 如果命令成功执行,system返回一个表示退出状态的值。
- 如果调用系统 shell 失败或命令无法执行,则返回 -1。
2.示例 :
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("exec init \n");
if(system("ps")==-1){
printf("exec = -1\n");
perror("system");
}
return 0;
}
在上述例子中,system("ps")
将调用系统的 shell 来执行 "ps" 命令。system函数会等待命令执行完成,然后返回命令的退出状态。如果调用失败,返回 -1。
需要注意的是,system函数的使用可能存在安全风险,特别是在处理用户输入时。如果你需要更加精细的控制和错误处理,可能需要使用更底层的函数,比如 fork
和 exec
组合。
这里可以看一下system的源码
system()函数功能强大,我们直接看linux版system函数的源码:
代码:
#include
#include
#include
#include
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid == 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
补充说明:
假设程序名是:sample_graph
那么system("sample_graph &");在其后面添加一个&符号表示程序在后台运行
7、popen函数
1.函数的作用
popen函数用于创建一个管道并打开一个新的进程或命令。它允许在一个进程中执行一个命令,并通过文件流进行输入和输出。popen返回一个文件指针,可以用于读取命令的输出或向命令传递输入
2.函数的具体运用
popen 会创建一个子进程来执行指定的命令。popen的工作机制涉及到使用 fork
创建一个子进程,然后在子进程中使用 exec
系列函数执行指定的命令。具体来说,popen会创建一个管道,并调用 fork
来创建一个子进程。在子进程中,通过调用 exec
函数来执行指定的命令,这就替换了子进程的映像为要执行的命令。父进程则可以通过管道与子进程通信,可以读取或写入子进程的标准输入和标准输出。在子进程执行完毕后,popen
会关闭相关的管道并等待子进程的结束。然后它会返回一个文件指针,通过这个文件指针你可以读取或写入子进程的标准输出或标准输入。
函数原型
FILE *popen(const char *command, const char *mode);
参数:
- command: 要执行的命令字符串。
- mode: 打开文件的模式,可以是 "r"(读取)或 "w"(写入)。
返回值:
- 如果成功,返回一个指向文件的指针。
- 如果失败,返回 NULL
3.示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
char dis[10240] = {0}; // 存储命令输出的缓冲区
FILE *file = NULL; // 文件指针用于处理命令输出流
// 使用popen打开一个管道并执行 "ls -l" 命令
file = popen("ls -l", "r");
if (file == NULL) {
perror("popen");
return -1;
}
// 读取命令输出到缓冲区,并获取读取的字节数,size_t无符号的整形
size_t bytesRead = fread(dis, 1, sizeof(dis) - 1, file);
if (bytesRead > 0) {
dis[bytesRead] = '\0'; // 在读取的字符串后添加字符串结束符,%z通常用于表示 size_t 类型的值
printf("Read %zu bytes:\n%s\n", bytesRead, dis);
} else {
printf("读取失败.\n");
}
// 关闭文件指针,等待子进程结束
pclose(file);
return 0;
}