Linux-----进程(多任务)
一、什么是进程
(一)进程的含义?
进程是一个程序执行的过程,会去分配内存资源,cpu的调度
(二)进程分类:
1、交互式进程
2、批处理进程 shell脚本
3、 守护进程
(三)进程与程序的区别
1)程序是永存,进程是暂时的
2)进程有程序状态的变化,程序没有
3)进程可以并发,程序无并发
4)进程与进程会存在竞争计算机的资源
5)一个程序可以运行多次,变成多个进程
一个进程可以运行一个或多个程序
(四)进程的作用:
并发并行(各执行各的)
(五)进程的状态:
3个状态,就绪→执行态→阻塞(等待,睡眠)基本操作系统
linux中的状态,运行态,睡眠态,僵尸态,暂停态。
(1)运行态(running):进程占有处理器正在运行。
(2)就绪态(ready):进程具备运行条件,等待系统分配处理器以便运行。
(3)阻塞态(blocked):又称为或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。
(4)僵尸态(zombie):子运行完,父没运行完,子进程会以终止状态保持在进程表中,并且会一直在等待父进程读取才能退出。
(5)孤儿态:父运行完,子没运行完,子处于托管状态,加重系统负担
(6)创建态:进程正在被创建,但尚未转到就绪态。
(7)终止态:进程已经完成执行并准备被撤销。
进程的状态:
PROCESS STATE CODES
Here are the different values that the s, stat and state output specifiers
(header "STAT" or "S") will display to describe the state of a process:
D uninterruptible sleep (usually IO) //不可中断的睡眠态
R running or runnable (on run queue) // 运行态
S interruptible sleep (waiting for an event to complete)//可中断的睡眠态
T stopped by job control signal // 暂停态
t stopped by debugger during the tracing
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z defunct ("zombie") process, terminated but not reaped by its parent
For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group
R --- 运行态
D --- 不可中断 睡眠态
S --- 可中断 睡眠态
T --- 暂停态
Z --- 僵尸态
cb
struct task_struct {
PID, //进程标识符
PPID, //父进程ID号 parent
当前工作路径 //chdir
umask //0002
进程打开的文件列表 //文件IO中有提到
信号相关设置 //处理异步io, ---段错误
用户id,组id
进程资源的上限
}
二、进程管理的命令
1、top //类似Windows的下任务管理器
2、ps -eLf | head -1 //可以观察到 PID PPID
3、ps -eLf | grep a.out //查看a.out 信息 //可以观察到 PID PPID
4、ps -aux | grep a.out //可以查看进程 的状态
5、pstree //进程树
6、pstree -sp pid号 //查看指定的进程的关系
7、kill //给进程发信号 //kill -l //查看可以发送的信号
操作:
将子进程杀死 。结束子进程,父进程还在,但是父进程并没有对子进程"收尸"
8、进程的pid号 ppid号 getpid getppid
操作:
将父进程杀死 。子进程 还在 ,父进程不在 ---- 孤儿进程
---- 此时由init进程 收养
特殊:
孤儿进程
子进程 还在,父进程不在
僵尸进程
子进程 结束,父进程还在,且父进程并未"收尸"
僵尸态进程有危害
三、函数
pid_t fork(void);
功能:
创建子进程 (通过复制调用进程)
参数:
void
返回值:
成功
在父进程中 返回子进程的pid号
在子进程中 返回0
失败
-1 && errno 被设置
pid号:
pid 本质上就是一个数值
正整数 1
#include<stdio.h>
#include<unistd.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid<0)
{
perror("fork fail");
return -1;
}
while(1)
{
if(pid > 0)
{
printf("hello\n");
}else if(pid ==0)
{
printf("world\n");
}
}
return 0;
}
1、子进程先运行和是父进程先进程,顺序不确定。
变量不共享。
2、子进程复制父进程的0到3g空间和父进程内核中的PCB,但id号不同。
3、此时,父子进程各自拥有独立的4g内存空间 (32位的系统)
4、功能
通过该函数可以从当前进程中克隆一个同名新进程。
克隆的进程称为子进程,原有的进程称为 父进程。
子进程是父进程的完全拷贝。
复制之后,
子进程和父进程 各自拥有自己的 用户空间(进程空间)
子进程和父进程的执行过程是从fork函数之后执行。
子进程与父进程具有相同的代码逻辑。
5、返回值:int 类型的数字。
在父进程中:成功 返回值是子进程的pid号 >0
失败 返回-1;
在子进程中:成功 返回值 0
失败 无
6、注意:
1.创建好之后,父子进程的运行顺序是不确定 ---全部取决于 操作系统的调度
注意:
1.父子进程创建好之后,各自拥有独立4G内存空间(32位系统)
2.他们的数据相互独立,父进程或子进程对数据的修改,不会相互影响,只会对各自造成影响
(一)、练习
如果两次fork同时前后执行,会生成几个进程?
fork();
fork();
他们之间的关系如何表示,
有多少个子进程,
有没有孙进程?
2、fork()&&fork()||fork(); //总共几个进程
#include<stdio.h>
#include<unistd.h>
int main(int argc, const char *argv[])
{
fork()&&fork()||fork();
while(1);
sleep(1);
return 0;
}
3、练习:
自己分别定义一个 static的变量 static int a = 0;
全局变量 int b = 1;
堆区 int *p = (int *)malloc(sizeof(int));
*p = 2;
(做修改)父进程中 做加1的操作 ,
子进程中做加2的操作
sleep(1);
分别打印,查看效果!
#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork();
static int a =0;
int b = 1;
int *p = (int *)malloc(sizeof(int));
*p = 2;
while(1)
{
if(pid > 0)
{
a++;
b++;
*p++;
sleep(1);
printf("father ");
printf("a = %d b = %d *p = %d\n",a,b,*p);
}else if(pid == 0)
{
a+=2;
b+=2;
*p+=2;
printf("sun ");
printf("a = %d b = %d *p = %d\n",a,b,*p);
sleep(1);
}
}
return 0;
}
4、创建n个进程 :
输入n 创建n个子进程
#include<stdio.h>
#include<unistd.h>
int main(int argc, const char *argv[])
{
int n = 0;
scanf("%d",&n);
int i = 0;
pid_t pid = 0;
for(i = 0;i < n;i++)
{
pid = fork();
if(pid < 0)
{
perror("fork fail");
return -1;
}
if(pid>0)
{
continue;
}else if(pid == 0)
{
break;
}
}
while(1)
sleep(1);
return 0;
}
5、创建n个进程,分批复制文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
void do_copy(int fd_s,int fd_d,int size,int i,int length)
{
char buf[1024];
//定位到要操作位置
lseek(fd_s,i*size,SEEK_SET);
lseek(fd_d,i*size,SEEK_SET);
int ret = read(fd_s,buf,length);
write(fd_d,buf,ret);
}
int main(int argc, const char *argv[])
{
if (argc != 3)
{
printf("Usage: %s <src> <dest>\n",argv[0]);
return -1;
}
int n = 0;
printf("Input proccess num:");
scanf("%d",&n);
int i = 0;
pid_t pid = 0;
for (i = 0; i < n;++i)
{
pid = fork();
if (pid < 0)
{
perror("fork fail");
return -1;
}
if (pid > 0)
{
continue;
}else if (pid == 0)
{
break;
}
}
if (pid > 0)
{
printf("father exit!\n");
return 0;
}else if (pid == 0)
{
int fd_s = open(argv[1],O_RDONLY);
if (fd_s < 0)
{
perror("open fail");
return -1;
}
int fd_d = open(argv[2],O_WRONLY|O_CREAT|O_EXCL,0666);
if (fd_d < 0)
{
if (errno == EEXIST)
{
fd_d = open(argv[2],O_WRONLY);
}else
{
perror("open fail");
return -1;
}
}
struct stat st;
if (stat(argv[1],&st) < 0)
{
perror("stat fail");
return -1;
}
printf("size = %ld\n",st.st_size);
int size = st.st_size/n;
int length = size;
if (i == n-1)
{
length = st.st_size - size*(n-1);
}
printf("i = %d each size = %d\n",i,size);
do_copy(fd_s,fd_d,size,i,length);
close(fd_s);
close(fd_d);
}
return 0;
}
(二)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
函数族用于在进程中执行另一个程序,替换当前进程的映像(代码段、数据段等),但保留进程的PID、文件描述符等资源。常用于结合 fork()
创建子进程后执行新程序。
函数原型 | 参数特点 | 是否搜索PATH | 环境变量传递 |
---|
int execl(const char *path, const char *arg0, ..., NULL) | 列表形式传递参数(逐个字符串) | 需要完整路径 | 继承父进程环境 |
int execv(const char *path, char *const argv[]) | 数组形式传递参数(argv[] ) | 需要完整路径 | 继承父进程环境 |
int execle(const char *path, const char *arg0, ..., NULL, char *const envp[]) | 列表形式 + 自定义环境变量 | 需要完整路径 | 使用 envp 参数传递 |
int execve(const char *path, char *const argv[], char *const envp[]) | 数组形式 + 自定义环境变量 | 需要完整路径 | 使用 envp 参数传递 |
int execlp(const char *file, const char *arg0, ..., NULL) | 列表形式,自动搜索PATH | 搜索PATH | 继承父进程环境 |
int execvp(const char *file, char *const argv[]) | 数组形式,自动搜索PATH | 搜索PATH | 继承父进程环境 |
2. 核心特点
- 替换性:
exec
成功后,原进程的代码段、数据段等被新程序完全替换,原进程后续代码不再执行。 - 不创建新进程:PID 保持不变,仅替换内存映像。
- 参数传递:
- 第一个参数一般为程序路径(
execlp
/execvp
可传文件名)。 - 参数列表必须以
NULL
结尾(防止越界)。
- 第一个参数一般为程序路径(
- 返回值:成功时无返回,失败返回
-1
,需通过errno
判断错误原因。
3. 典型用法示例
示例1:execl
执行 /bin/ls
#include <unistd.h>
#include <stdio.h>
int main() {
execl("/bin/ls", "ls", "-l", NULL); // 参数列表必须以NULL结尾
perror("execl failed"); // 若执行失败才会执行到这里
return 1;
}
示例2:execvp
搜索PATH执行 ls
#include <unistd.h>
int main() {
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv); // 自动搜索PATH中的ls
return 1; // 仅失败时执行
}
示例3:结合 fork()
创建子进程
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == 0) { // 子进程
execlp("ls", "ls", "-l", NULL);
perror("execlp error");
_exit(1); // 子进程退出
} else { // 父进程
wait(NULL); // 等待子进程结束
printf("Child process finished.\n");
}
return 0;
}
示例4:myshell
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>
int main(int argc, const char *argv[])
{
char buf[1024];
while(1)
{
printf("myshell$ ");
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1] = '\0';
if(strncmp(buf,"exit",4)==0 || strncmp(buf,"quit",4) == 0)
{
printf("-----exit---\n");
return 0;
}
//解析
int i = 0;
char *a[20] = {NULL};
a[0] = strtok(buf," ");
while(a[++i] = strtok(NULL," "));
//fork
pid_t pid = fork();
if(pid < 0)
{
perror("fork faill");
return -1;
}
if(pid > 0)
{
wait(NULL);
}else if(pid == 0)
{
if(execvp(a[0],a) < 0);
{
perror("execvp fail");
return -1;
}
}
}
return 0;
}
4. 注意事项
- 参数列表必须以
NULL
结尾,否则可能导致未定义行为。 - 环境变量:
execle
和execve
可自定义环境变量(通过envp
参数)。 - 错误处理:
exec
成功后不会返回,因此错误处理代码必须放在调用前。
(三)strtok
strtok()
是字符串分割函数,用于将字符串按指定分隔符拆分成多个子字符串(Token)。常用于解析文本数据(如CSV、日志文件等)。
1. 函数原型
#include <string.h>
char *strtok(char *str, const char *delim);
2. 参数说明
参数 | 说明 |
---|---|
str | 待分割的字符串(首次调用时传入,后续调用传 NULL ) |
delim | 分隔符集合(多个字符均可作为分隔符) |
3. 返回值
- 成功时返回指向子字符串的指针。
- 无更多子字符串时返回
NULL
。
4. 核心特性
- 静态指针:内部维护静态指针记录当前分割位置,不可重入(非线程安全)。
- 修改原字符串:将分隔符替换为
'\0'
,破坏原字符串。 - 连续分隔符:自动跳过连续的分隔符。
- 线程安全替代:
strtok_r
(POSIX标准,需额外传递上下文指针)。
5. 使用步骤
- 首次调用:传入待分割字符串和分隔符。
- 后续调用:传入
NULL
和分隔符,继续分割。 - 结束条件:返回
NULL
时表示分割完成。
6. 示例代码
示例1:分割逗号分隔的字符串
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "apple,banana,,grape";
char *token = strtok(str, ","); // 首次调用传str
while (token != NULL) {
printf("Token: %s\n", token);
token = strtok(NULL, ","); // 后续调用传NULL
}
return 0;
}
输出:
Token: apple
Token: banana
Token: grape
示例2:多分隔符(空格和逗号)
char str[] = "hello world;foo|bar";
char *token = strtok(str, " ;|");
while (token) {
printf("%s\n", token);
token = strtok(NULL, " ;|");
}
输出:
hello
world
foo
bar
7. 注意事项
- 原字符串被修改:分隔符会被替换为
'\0'
,需提前拷贝原始字符串(如用strdup()
)以保留原数据。char *copy = strdup(original_str); token = strtok(copy, delim);
- 线程安全问题:
strtok
不可重入,多线程环境下需用strtok_r
:char *strtok_r(char *str, const char *delim, char **saveptr);
- 空子字符串:若字符串以分隔符开头或连续分隔符,默认跳过空子字符串(如示例1中的
",,"
被忽略)。 - 静态指针陷阱:不可嵌套调用(同一线程内交替分割不同字符串会导致混乱)。
8. 常见错误
- 未检查返回值:直接对返回的指针操作,未处理
NULL
。 - 错误的分隔符传递:分隔符需为字符串形式(如
","
而非','
)。 - 跨函数调用失效:因静态指针状态被其他
strtok
调用破坏。
9. 替代方案
方法 | 说明 |
---|---|
strsep() | BSD扩展函数,更灵活但需手动处理空指针(如 while ((token = strsep(&ptr, delim)) != NULL) ) |
自定义分割 | 手动遍历字符串,记录子字符串起始和结束位置(内存安全但代码复杂) |
(四) exit()
函数
exit()
用于正常终止程序,执行清理操作(如刷新缓冲区、关闭文件等),并将退出状态返回给操作系统。常用于程序主动结束(如完成任务或遇到可处理错误)。
1. 函数原型
#include <stdlib.h>
void exit(int status);
2. 参数说明
参数 | 说明 |
---|---|
status | 退出状态码:<br>- EXIT_SUCCESS (通常为0)表示成功<br>- EXIT_FAILURE (通常为1)表示失败 |
3. 核心特性
- 清理操作:
- 调用通过
atexit()
注册的函数(按注册的逆序执行)。 - 刷新所有已打开的I/O缓冲区(如
printf
未换行时强制输出)。 - 关闭所有文件流(如
fopen
打开的文件)。
- 调用通过
- 不返回:调用后进程终止,控制权交还给操作系统。
- 状态码传递:父进程可通过
wait()
或 shell 的$?
获取状态码。
4. 使用示例
示例1:正常退出
#include <stdlib.h>
int main() {
printf("Program starts.\n");
exit(EXIT_SUCCESS); // 或 exit(0)
printf("This line will NOT execute.\n");
}
示例2:结合 atexit()
注册清理函数
#include <stdlib.h>
#include <stdio.h>
void cleanup1() { printf("Cleanup 1\n"); }
void cleanup2() { printf("Cleanup 2\n"); }
int main() {
atexit(cleanup1);
atexit(cleanup2); // 注册顺序:cleanup2 → cleanup1
printf("Main function\n");
exit(EXIT_SUCCESS);
}
输出:
Main function
Cleanup 2
Cleanup 1
5. exit()
vs _exit()
特性 | exit() | _exit() |
---|---|---|
头文件 | <stdlib.h> | <unistd.h> |
清理操作 | 执行缓冲区刷新、atexit() 等 | 直接终止,无清理 |
使用场景 | 正常退出 | 子进程终止(避免重复清理) |
示例 | exit(0) | _exit(1) |
示例:exit()
与 _exit()
对比
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("Hello"); // 无换行符,依赖缓冲区刷新
// exit(0); // 输出 "Hello"
_exit(0); // 无输出
}
6. 常见问题
Q1:exit()
和 return
的区别?
exit()
:终止整个进程,可在任何函数中调用。return
:退出当前函数,若在main()
中return
会隐式调用exit()
。
Q2:状态码的取值?
- 0-255(Unix/Linux约定),0表示成功,非0表示错误类型。
- 建议使用宏
EXIT_SUCCESS
和EXIT_FAILURE
。
Q3:如何获取子进程的退出状态?
通过 wait()
和 WEXITSTATUS
宏:
#include <sys/wait.h>
int status;
pid_t pid = fork();
if (pid == 0) exit(42);
wait(&status);
printf("Child exit code: %d\n", WEXITSTATUS(status)); // 输出42
7. 注意事项
- 避免多次调用:
exit()
后程序终止,后续代码不执行。 - 线程安全:
exit()
终止整个进程(包括所有线程)。 - 信号处理:若程序因信号终止(如
SIGSEGV
),exit()
不会被调用。
(五) wait()
函数
wait()
用于父进程等待子进程终止,并回收子进程资源(避免僵尸进程)。通过该函数可获取子进程的退出状态。
1. 函数原型
#include <sys/wait.h>
pid_t wait(int *status);
2. 参数说明
参数 | 说明 |
---|---|
status | 输出参数,存储子进程的退出状态(可用宏解析状态码)<br>若为 NULL ,表示不关心状态 |
3. 返回值
返回值 | 说明 |
---|---|
>0 (pid_t) | 成功,返回被终止的子进程PID |
-1 | 失败(如无子进程) |
4. 核心特性
- 阻塞等待:父进程调用
wait()
后阻塞,直到任一子进程终止。 - 资源回收:释放子进程的PCB(进程控制块)等内核资源。
- 状态解析:通过宏(如
WIFEXITED
、WEXITSTATUS
)解析status
。
5. 状态码解析宏
宏 | 说明 |
---|---|
WIFEXITED(status) | 若子进程正常退出(通过 exit() 或 return )返回真 |
WEXITSTATUS(status) | 若 WIFEXITED 为真,返回子进程的退出状态码(exit() 的参数) |
WIFSIGNALED(status) | 若子进程因信号终止返回真(如 SIGKILL ) |
WTERMSIG(status) | 若 WIFSIGNALED 为真,返回导致终止的信号编号 |
6. 使用示例
示例1:父进程等待单个子进程
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == 0) { // 子进程
printf("Child PID: %d\n", getpid());
sleep(2);
exit(123); // 子进程退出码123
} else { // 父进程
int status;
pid_t child_pid = wait(&status);
if (WIFEXITED(status)) {
printf("Child %d exited with code: %d\n",
child_pid, WEXITSTATUS(status)); // 输出123
}
}
return 0;
}
示例2:等待所有子进程
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
for (int i = 0; i < 3; i++) {
if (fork() == 0) {
printf("Child %d\n", getpid());
exit(0);
}
}
int status;
pid_t pid;
while ((pid = wait(&status)) != -1) { // 循环等待所有子进程
printf("Child %d exited\n", pid);
}
return 0;
}
7. 常见错误
- 忽略子进程退出:未调用
wait()
导致僵尸进程。 - 多次调用
wait()
:若子进程数量不足,后续调用返回-1
(错误码ECHILD
)。 - 错误处理缺失:未检查
wait()
返回值,误判子进程状态。
8. 僵尸进程与孤儿进程
- 僵尸进程:子进程终止后,父进程未调用
wait()
回收其资源。 - 孤儿进程:父进程先于子进程终止,子进程被
init
进程(PID=1)接管并自动回收。
9. 注意事项
- 信号干扰:若父进程被信号中断,
wait()
可能返回-1
(错误码EINTR
),需重试。 - 非阻塞模式:使用
waitpid()
的WNOHANG
选项轮询子进程状态。 - 多线程安全:
wait()
在多线程环境中应避免竞争条件。