进程是操作系统中的一个基本概念,它是正在运行的程序的实例。进程不仅仅是代码,还包括代码执行时所需的资源和状态信息。 简单来说进程=程序的代码和数据+内核数据结构(内核数据结构用于管理进程的资源和状态等信息)
由于上面我们说到进程等于内核数据结构加上自己的代码和数据,这里的数据结构在Linux中叫做task_struct,task_struct是PCB的一种。
PCB是操作系统中用于管理每个进程的重要数据结构。它包含了操作系统需要的所有信息,用来跟踪、控制和调度进程。每个进程都会对应一个唯一的PCB,操作系统通过PCB来识别和管理进程的状态和资源。
首先我们知道一个程序在运行时都是要先被加载到内存中的,然后加载到内存中之后由CPU进行读取数据。加载到内存中,我们是不是只加载了程序的代码,很显然不是的,如果我们只加载代码和数据,那我们该怎么管理这个进程呢?所以我们还需要一推数据来方便管理这个进程,比如说这个进程的状态,标识符,优先级等等信息,这些信息被合起来创建了一个task_struct,来管理这个进程。 这里展示部分task_struct的代码:
struct task_struct {
/*
* These are the only fields we actually need to create a task:
*/
struct list_head tasks; /* 指向同一进程组中所有任务的列表头 */
pid_t pid; /* 进程ID */
int state; /* 进程状态 */
unsigned int flags; /* 进程标志 */
struct task_struct __rcu *real_parent; /* 父进程PCB指针 */
struct task_struct *parent; /* 父进程PCB指针,即使父进程已退出也不会为NULL */
struct list_head children; /* 子进程链表 */
struct list_head sibling; /* 兄弟进程链表 */
/*
* These fields are here for internal use. They should not be
* touched outside of the scheduler proper.
*/
struct thread_info *thread_info;
struct fs_struct *fs; /* 文件系统结构体 */
struct files_struct *files; /* 打开文件结构体 */
struct signal_struct *signal; /* 进程信号管理结构体 */
struct sighand_struct *sighand; /* 信号处理函数集合 */
struct nsproxy *nsproxy;
struct cred *cred;
struct cred *real_cred;
u64 start_time; /* 进程开始时间 */
struct timer_list real_timer;
struct pid_link pids[PIDTYPE_MAX];
struct taskstats *stats; /* 与进程相关的统计信息 */
可以看见管理进程的代码中有很多进程的信息。
在task_struct中有一个指针指向下一个进程,还有一个指针指向自己对应的进程
首先我们来说说标识符信息。
标识符是什么?
先写一段简单的死循环代码。
用上面指令加管道查看指定进程。
ps ajx | grep myexe
首先,ps ajx是查看所有的进程,后面加上管道就可以查看指定的进程
可以看见查看所有进程的时候第一排是有头部信息的,而我们的没有,所以我们可以把头部信息加上。
ps ajx | head -1 && ps ajx | grep myexe
这段指令就可以使得查看指定进程的同时顺便加上头部信息了。 可以看见,这里的头部信息有很多,pid就是我们所说的标识符,标识符具有唯一性,可以唯一的确定一个进程,意思就是我们可以通过标识符来查找进程。
可以看见确实可以用pid来查看指定进程,这里可以看见多出来一个bash进程,通过观察可以看见myexe的ppid和bash的pid是相同的,可以发现bash和myexe是父子进程,bash是命令解释器,myexe是bash的子进程。意思就是说在启动myexe的时候,bash启动了一个myexe的子进程。 ppid ppid是父进程(parent pid)可以这样理解
直到怎么查看进程之后,我们应该如何结束进程呢?
第一种方式:Ctrl+c 第二种方式:kill+进程标识符
这两种方式都可以结束进程。
我们已经知道了bash会创建一个子进程来执行我们的命令,那么我们该如何手动创建一个子进程呢?
通过上面的函数fork()可以手动创建一个子进程。
可以看见创建成功会给父进程返回子进程的pid,给子进程返回0,如果创建失败会返回-1。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 pid_t id=fork();
7 while(1)
8 {
9 if(id>0)
10 {
11 printf("我是父进程,pid:%d ,ppid:%d\n",getpid(),getppid());
12 sleep(1);
13 }
14 else if(id==0)
15 {
16 printf("我是子进程,pid:%d ,ppid:%d\n",getpid(),getppid());
17 sleep(1);
18 }
19 }
20 return 0;
21 }
上面代码可以创建子进程,我们来看看现象。 运行结果看看现象:
可以看见子进程和父进程都打印了,看看上面代码,明明这是一个if和else if为什么会两个条件同时成立呢?
因为这里创建了一个子进程,子进程和父进程共享同一份代码,但是数据是私有的,所以会产生这样的结果,我们来验证一下是不是。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int gval;
5
6 int main()
7 {
8 pid_t id=fork();
9 while(1)
10 {
11 if(id>0)
12 {
13 printf("我是父进程,pid:%d ,ppid:%d ,id:%d ,gval:%d\n",getpid(),getppid(),id,gval);
14 sleep(1);
15 }
16 else if(id==0)
17 {
18 printf("我是子进程,pid:%d ,ppid:%d ,id:%d ,gval:%d\n",getpid(),getppid(),id,gval);
19 gaval++;
20 sleep(1);
21 }
22 }
23 return 0;
24 }
首先这里创建一个全局变量,子进程负责++和读取,但是父进程只读取,如果是同一份数据的话,那么全局变量两个应该打出来是同一个值,这里我们运行验证一下。
可以看见只有子进程的++了,父进程并没有++,可以看见两个进程的数据是私有的,这里我们可以得出一个结论:两个进程之间是具有高度独立性的。
验证完这个之后,我们该如何创建多进程呢?
1 #include<iostream>
2 #include<vector>
3 #include<unistd.h>
4 #include<sys/types.h>
5 using namespace std;
6
7 int num=10;
8
9 void SubProcess()
10 {
11 while(true)
12 {
13 cout<<"I am sub process, pid:"<<getpid()<<", ppid:"<<getppid()<<endl;
14 sleep(1);
15 }
16 }
17
18 int main()
19 {
20 vector<int> allchild;
21 for(int i=0;i<num;i++)
22 {
23 pid_t id=fork();
24 //子进程进入
25 if(id==0)
26 {
27 SubProcess();
28 }
29 //到这里只有父进程
30 allchild.push_back(id);
31 }
32 cout<<endl;
33 for(auto child:allchild)
34 {
35 cout<<child<<' ';
36 }
37 cout<<endl;
38 while(true)
39 {
40 cout<<"我是父进程:pid:"<<getpid()<<" ,ppid: "<<getppid()<<endl;
41 sleep(1);
42 }
43
44 return 0;
45 }
看看对应信息确实对应起来了
可以看见确实创建了十个进程。
本文从进程的基本概念入手,介绍了进程的组成结构,尤其是PCB(进程控制块)的作用。通过分析 task_struct 的内容,我们了解了进程在内核中的重要数据结构如何帮助管理其状态和资源。随后,我们探讨了多进程的创建过程,并通过代码实例展示了多进程的实现。总的来说,进程是操作系统管理资源的关键单元,深入理解其结构和机制对于系统级编程至关重要。 通过这篇文章,希望读者能够对进程的内部运作和管理有更加清晰的认识,为日后深入学习操作系统或编写并发程序打下基础。