Day29 基于fork+exec的minishell实现与pthread多线程
day29 基于fork+exec的minishell实现与pthread多线程
使用fork+exec实现minishell
核心功能说明
本程序实现了一个简易的shell,具有以下功能:
- 显示当前工作目录提示符
- 接收并解析用户输入的命令
- 使用fork创建子进程执行命令
- 使用execvp执行系统命令
- 特别处理
cd
和ll
命令 - 避免产生僵尸进程(通过wait(NULL))
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库(包含exit等函数)
#include <string.h> // 字符串操作库
#include <sys/wait.h> // 等待子进程相关函数
#include <unistd.h> // POSIX系统调用(getcwd, fork等)// 显示命令提示符函数
void show_prompt()
{char path[512] = {0}; // 用于存储当前工作目录的缓冲区getcwd(path, sizeof(path)); // 获取当前工作目录printf("linux@ubuntu:%s$", path); // 打印标准提示符格式
}int main(int argc, char **argv)
{while (1) // 无限循环,保持shell持续运行{show_prompt(); // 显示命令提示符char cmd_line[256] = {0}; // 存储用户输入的命令行fgets(cmd_line, sizeof(cmd_line), stdin); // 读取用户输入cmd_line[strlen(cmd_line) - 1] = '\0'; // 移除输入末尾的换行符// 如果输入为空行,跳过处理if (0 == strlen(cmd_line)){continue;}// 用于存储命令参数的指针数组(最多4个参数)char *args[5] = {NULL};// 使用strtok分割命令行,以空格为分隔符args[0] = strtok(cmd_line, " "); // 第一个参数(命令名)args[1] = strtok(NULL, " "); // 第二个参数args[2] = strtok(NULL, " "); // 第三个参数args[3] = strtok(NULL, " "); // 第四个参数// 特殊处理cd命令if (0 == strcmp(args[0], "cd")){// 如果没有参数,切换到/home/linux目录if (NULL == args[1]){chdir("/home/linux");}else // 有参数,切换到指定目录{chdir(args[1]);}continue; // cd命令处理完毕,跳过后续执行}// 创建子进程pid_t pid = fork();if (0 == pid) // 子进程代码{// 特殊处理ll命令(将其转换为ls -alhF)if (0 == strcmp(args[0], "ll")){if (NULL == args[1]) // 如果没有参数{args[0] = "ls"; // 命令名改为"ls"args[1] = "-alhF"; // 添加参数"-alhF"}else // 有参数{args[0] = "ls"; // 命令名改为"ls"args[2] = "-alhF"; // 将"-alhF"作为第三个参数}}// 执行命令,如果失败则退出子进程execvp(args[0], args);exit(0);}// 父进程等待子进程结束,避免僵尸进程wait(NULL);}return 0;
}
理想运行结果
linux@ubuntu:/home/linux$ ls /home/linux
file1.txt file2.c directory1
linux@ubuntu:/home/linux$ cd directory1
linux@ubuntu:/home/linux/directory1$ ll
total 12
drwxr-xr-x 2 user user 4096 Aug 18 10:00 .
drwxr-xr-x 3 user user 4096 Aug 18 09:00 ..
-rw-r--r-- 1 user user 23 Aug 18 10:00 file3.txt
linux@ubuntu:/home/linux/directory1$ ls -l
total 4
-rw-r--r-- 1 user user 23 Aug 18 10:00 file3.txt
linux@ubuntu:/home/linux/directory1$
关键说明:
- 程序会显示类似
linux@ubuntu:/当前目录$
的提示符 - 输入
ls /home/linux
会列出指定目录下的文件 - 输入
cd directory1
会切换到子目录 - 输入
ll
会被自动转换为ls -alhF
并执行 - 程序通过
wait(NULL)
确保子进程结束后才继续,避免僵尸进程
pthread 线程
基本概念
线程定义:线程是轻量级进程,是进程中的多个并发执行任务。
核心区别:
- 进程:系统中最小的资源分配单位
- 线程:系统中最小的执行单位
主要优点:
- 比多进程节省系统资源
- 线程间可以直接共享变量(除栈区外的内存区域)
主要缺点:
- 稳定性比进程稍差
- 调试相对复杂(需使用
info thread
等GDB命令)
线程与进程对比
特性 | 进程 | 线程 |
---|---|---|
资源开销 | 约3GB | 约8MB |
数据共享 | 需要IPC机制 | 除栈区外全部共享 |
稳定性 | 高 | 相对较低 |
适用场景 | 独立大任务 | 同一任务中的小任务 |
归属关系 | 独立实体 | 属于某个进程 |
共同点:都支持并发执行。
线程核心操作
1. 线程创建
int pthread_create(pthread_t *thread, // 线程ID存储位置const pthread_attr_t *attr, // 线程属性(NULL表示默认)void *(*start_routine)(void *), // 线程执行函数void *arg // 传递给执行函数的参数
);
功能:创建新线程
返回值:成功返回0,失败返回错误码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>// 线程1执行函数
void *th1(void *arg)
{while(1){printf("th1 发送视频\n");sleep(1);}return NULL; // 线程正常退出
}// 线程2执行函数
void *th2(void *arg)
{while(1){printf("th2 接收控制\n");sleep(1);}return NULL; // 线程正常退出
}int main(int argc, char **argv)
{pthread_t tid1, tid2; // 线程ID变量// 创建两个线程,分别执行th1和th2pthread_create(&tid1, NULL, th1, NULL);pthread_create(&tid2, NULL, th2, NULL);while(1) sleep(1); // 主线程保持运行return 0;
}
关键说明:
- 一次
pthread_create
只能创建一个线程 - 每个进程至少有一个线程(主线程)
- 主线程退出会导致所有子线程退出
- 使用
ps -eLf
或ps -eLo pid,ppid,lwp,stat,comm
可查看线程信息
2. 获取线程ID
pthread_t pthread_self(void);
功能:获取当前线程的ID
返回值:当前线程ID
// 在线程函数中获取并打印线程ID
printf("th1 发送视频,tid :%lu\n", pthread_self());
printf("th2 接收控制,tid :%lu\n", pthread_self());
printf("main_th tid: %lu\n", pthread_self());
3. 线程退出方式
3.1 自行退出(自杀)
void pthread_exit(void *retval);
功能:线程自行退出,可返回状态值
void* th(void* arg)
{int i = 3;while (i--){printf("th tid:%lu\n", pthread_self());sleep(1);}pthread_exit(NULL); // 线程退出
}
3.2 强制退出(他杀)
int pthread_cancel(pthread_t thread);
功能:请求结束指定线程
void* th(void* arg)
{while (1){printf("th tid:%lu\n", pthread_self());sleep(1);}
}int main(int argc, char** argv)
{pthread_t tid;pthread_create(&tid, NULL, th, NULL);int i = 0;while (1){printf("main_th tid:%lu\n", pthread_self());sleep(1);i++;if(3 == i) // 3秒后取消子线程{pthread_cancel(tid);}}return 0;
}
4. 线程回收
int pthread_join(pthread_t thread, void **retval);
功能:阻塞等待线程结束并回收资源
参数:
thread
:要回收的线程IDretval
:接收线程退出状态的指针
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>void* th(void* arg)
{ int i = 5;while(i--){printf("th processing\n");sleep(1);}// char str[] = "我要结束了"; // 错误:局部变量,函数返回后空间释放// static char str[] = "我要结束了"; // 不推荐:多线程可能导致数据覆盖char* str = malloc(20); // 正确:在堆上分配内存strcpy(str, "我要结束了"); // 复制字符串到堆内存pthread_exit(str); // 退出线程并返回堆内存地址
}int main(int argc, char **argv)
{pthread_t tid; // 线程ID变量pthread_create(&tid, NULL, th, NULL); // 创建线程void* ret = NULL; // 用于接收线程返回值pthread_join(tid, &ret); // 阻塞等待线程结束printf("ret %s\n", (char*)ret); // 强制类型转换并打印free(ret); // 释放线程分配的堆内存return 0;
}
线程回收策略:
- 预估线程能有限时间内结束:使用
pthread_join
等待 - 线程可能休眠或阻塞:等待一定时间后强制处理
- 线程需长时间运行:不回收资源或设置分离属性
5. 线程参数与返回值
5.1 基本参数传递
void* th(void* arg)
{char* tmp = (char*)arg;strcpy(tmp, "hello,world");return tmp;
}int main(int argc, char** argv)
{pthread_t tid = 0;char* p = malloc(20);pthread_create(&tid, NULL, th, p); // 传递堆内存地址void* ret = NULL;pthread_join(tid, &ret);printf("ret is %s\n", (char*)ret);return 0;
}
5.2 结构体参数传递
typedef struct
{char name[50]; // 姓名字段(50字节缓冲区)int age; // 年龄字段char addr[100]; // 地址字段(100字节缓冲区)
} PER;void* th(void* arg)
{PER* per = (PER*)arg; // 将参数转换为结构体指针printf("input name:");fgets(per->name, sizeof(per->name), stdin); // 读取姓名(含换行符)per->name[strlen(per->name)-1] = '\0'; // 移除换行符printf("input age:");char tmp[5] = {0}; // 临时缓冲区存储年龄fgets(tmp, sizeof(tmp), stdin);per->age = atoi(tmp); // 转换为整数printf("input addr:");fgets(per->addr, sizeof(per->addr), stdin); // 读取地址return per; // 返回结构体指针
}int main(int argc, char **argv)
{PER per; // 在主线程栈上创建结构体pthread_t tid;pthread_create(&tid, NULL, th, &per); // 传递结构体地址void* ret = NULL;pthread_join(tid, &ret);// 打印线程填充的结构体数据printf("name:%s age:%d addr:%s\n", ((PER*)ret)->name, ((PER*)ret)->age, ((PER*)ret)->addr);return 0;
}
参数传递方式:
- 整数:直接传递(需注意类型转换)
- 字符串:
- 栈区字符数组:不推荐(线程结束后失效)
- 字符串常量:全局有效但不可修改
- 堆区字符串:推荐(需手动释放)
- 结构体:
- 定义结构体类型
- 创建结构体变量
- 传递结构体地址
- 在线程函数中访问结构体数据
返回值处理:
- 返回指针必须指向有效内存(堆内存或全局/静态变量)
- 栈区变量返回会导致未定义行为
- 主线程需负责释放堆内存
6. 线程分离属性
int pthread_attr_init(pthread_attr_t *attr); // 初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr); // 销毁线程属性
void* th(void* arg)
{pthread_detach(pthread_self()); // 设置线程为分离状态return NULL;
}int main(int argc, char **argv)
{pthread_t tid;int ret = pthread_create(&tid, NULL, th, NULL);// 无需pthread_join,线程结束自动回收资源return 0;
}
作用:线程消亡后自动回收资源,无需pthread_join
7. 线程清理函数
void pthread_cleanup_push(void (*routine)(void *), void *arg); // 注册清理函数
void pthread_cleanup_pop(int execute); // 调用清理函数
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>// 清理函数使用的结构体
typedef struct
{FILE* fp;char* p;
} TH_ARGS;// 清理函数
void clean(void *arg)
{TH_ARGS* tmp = (TH_ARGS*)arg;printf("this clean, p is %s\n", tmp->p);free(tmp->p);fclose(tmp->fp);
}void* th(void* arg)
{FILE* fp = fopen("1.txt", "w"); // 打开文件char* p = malloc(20); // 分配堆内存TH_ARGS args = {0};args.fp = fp;args.p = p;pthread_cleanup_push(clean, &args); // 注册清理函数fputs("hello", fp); // 写入文件strcpy(p, "hello"); // 复制字符串printf("th well done\n");pthread_cleanup_pop(1); // 执行清理函数return NULL;
}int main(int argc, char **argv)
{pthread_t tid;pthread_create(&tid, NULL, th, NULL);pthread_join(tid, NULL);printf("main th will exit...\n");return 0;
}
功能:在线程退出时自动执行清理操作(如释放资源)
线程与进程函数对比
进程操作 | 线程操作 |
---|---|
fork | pthread_create |
getpid, ppid | pthread_self |
exit | pthread_exit |
wait, waitpid | pthread_join |
kill | pthread_cancel |
atexit | pthread_cleanup |
exec | system(内部使用fork+exec) |
线程核心要点回顾
- 线程本质:轻量级进程,属于某个进程
- 核心优势:
- 资源开销小(约8MB vs 进程3GB)
- 数据共享方便(除栈区外全部共享)
- 适用场景:并发执行同一任务中的小任务
- 关键函数:
- 创建:
pthread_create
- 回收:
pthread_join
- 传参:通过void*指针传递任意数据
- 创建:
- 内存管理:线程返回值必须指向有效内存(堆内存推荐)
- 调试技巧:GDB中使用
info thread
查看线程状态