当前位置: 首页 > article >正文

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. 注意事项
  1. 参数列表必须以 NULL 结尾,否则可能导致未定义行为。
  2. 环境变量execle 和 execve 可自定义环境变量(通过 envp 参数)。
  3. 错误处理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. 使用步骤
  1. 首次调用:传入待分割字符串和分隔符。
  2. 后续调用:传入 NULL 和分隔符,继续分割。
  3. 结束条件:返回 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. 注意事项
  1. 原字符串被修改:分隔符会被替换为 '\0',需提前拷贝原始字符串(如用 strdup())以保留原数据。
    char *copy = strdup(original_str);
    token = strtok(copy, delim);
    
  2. 线程安全问题strtok 不可重入,多线程环境下需用 strtok_r
    char *strtok_r(char *str, const char *delim, char **saveptr);
    
  3. 空子字符串:若字符串以分隔符开头或连续分隔符,默认跳过空子字符串(如示例1中的 ",," 被忽略)。
  4. 静态指针陷阱:不可嵌套调用(同一线程内交替分割不同字符串会导致混乱)。

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. 注意事项
  1. 避免多次调用exit() 后程序终止,后续代码不执行。
  2. 线程安全exit() 终止整个进程(包括所有线程)。
  3. 信号处理:若程序因信号终止(如 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(进程控制块)等内核资源。
  • 状态解析:通过宏(如 WIFEXITEDWEXITSTATUS)解析 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. 注意事项
  1. 信号干扰:若父进程被信号中断,wait() 可能返回 -1(错误码 EINTR),需重试。
  2. 非阻塞模式:使用 waitpid() 的 WNOHANG 选项轮询子进程状态。
  3. 多线程安全wait() 在多线程环境中应避免竞争条件。


http://www.kler.cn/a/553891.html

相关文章:

  • Pycharm中查找与替换
  • 介绍cherrypick
  • FFmpeg 源码编译安装
  • Zookeeper(58)如何在Zookeeper中实现分布式锁?
  • 《剑指数据库:MySQL安装布阵全解》
  • 1.buuctf [BJDCTF2020]EasySearch
  • 【c++】c++内存管理
  • 老游戏回顾:d2
  • 数据库连接管理--Java连接数据库的几种方式
  • python concurrent.futures
  • 【2025最新计算机毕业设计】基于SSM的社区老人服务平台 可定制开发【提供源码+答辩PPT+文档+项目部署】
  • 【LLAMA】羊驼从LLAMA1到LLAMA3梳理
  • SpringBoot+uniApp日历备忘录小程序系统 附带详细运行指导视频
  • [Android]文件描述符的binder传送
  • 迅为iTOP-RK3576开发板/核心板6TOPS算力4K视频编解码
  • Redis 键对应的命令详解
  • mysql实现原理 - 字符集和排序规则
  • Python安装与环境配置全程详细教学(包含Windows版和Mac版)
  • [网络] 如何开机自动配置静态IP,并自动启动程序
  • 第六步:Python协议与模块——当字典化身数据库,import玩出花