Linux是最受程序员欢迎的操作系统之一。第一它是开源的,第二它的系统调用少,第三它的抽象更到位。一切皆进程,一切皆文件。这两个“一切”已经把Linux的基调表达的充分无疑。统一接口就是对用户最大的友善。我想没有一个程序员愿意学习动辄就上千个系统调用,还不知道是怎么实现的操作系统。
进程及线程出现的背景
如果想要深入理解一个事物的本质,最好的方式就是去追寻这个事物出现的历史背景和推动因素。
- 最早的计算机是没有操作系统的,只有输入、计算和输出功能,用户输入一个指令,计算机完成操作,大部分时间计算机都在等待用户输入指令,这样的处理性能显然是很低效的,因为人的输入速度是远远比不上计算机的运行速度的。
- 为了解决手工操作带来的低效,批处理操作系统应用而生。批处理简单来说就是把要执行的指令记录下来形成一个指令清单,然后交给计算机去处理。这可以很大提升效率。
- 批处理有一个明显的缺点:计算机一次只能执行一个任务,如果某个任务是IO操作,那么CPU就要等这个任务完成,才能执行下一个任务。但其实这个时候CPU是空闲的,会造成CPU资源浪费。
- 为了进一步提升性能,发明了“进程”,用进程来对应一个任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。注意:此时的CPU并没有多核的概念,为了达到多进程并行运行的目的,采用了时间片机制。每个片段只能执行某个进程中的指令。 虽然从操作系统和CPU的角度来说还是串行处理的,但是由于CPU的处理速度很快,从用户的角度来看,感觉是多进程在并行处理。
- 多进程虽然要求每个任务都有独立的内存空间,进程间互不相关,但从用户的角度来看,两个任务之间能够在运行过程中就进行通信,会让任务设计变得更加灵活高效。否则如果两个任务设计过程中不能通信,只能是A任务将结果写到存储,B任务再从存储读取进行处理,不仅效率低,而且设计同步与互斥也相当复杂。为了解决这个问题,进程间的通信被设计出来,包括管道、消息队列、信号量、共享内存、socket等。
- 多进程让多任务能够并行处理,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求严格按照时间顺序来执行,也需要并行处理。怎么办呢?当然断续拆解和细分任务是一个解决方案,但是细化总会有个度,如果创建任务的成本比任务之间的通信的成本还高,就没有必要细化下去了, 但还是有并行处理的需求。线程就闪亮登场了。线程是进程内部的子任务,共享进程同一份进程数据。
- 为了保证数据的正确性,又发明了互斥锁和同步机制。
有了线程,操作系统调度的最小单元就变成了线程,而进程变成了操作系统分配资源的最小单元。有了多核CPU,任务执行也不再是分时系统,多个线程可以同时在多个CPU上运行,此时才做到时间上的真正并行,目前操作系统处理多核CPU的方案最主要是SMP(Symmetric Multi-Processor对称多处理器结构)。
linux进程
1. 进程的创建
linux使用fork创建一个进程。linux系统运行起来后会创建一个为编号为1的init的进程。后面的进程都是init进程的后代。
#include
#include
int main(int argc,char** argv) {
int pid = fork();
printf("pid = %d\n",pid);
if (pid == 0) {
printf("pid = %d,my parent pid = %d\n",getpid(),getppid());
} else {
printf("pid = %d,my parent pid = %d\n",getpid(),getppid());
wait(NULL);
}
return 0;
}
这个函数为返回两次,如果0返回给子进程,如果大于0返回给父进程子进程的pid。
2. 进程运行状态机
运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
3.进程回收
回收原则:谁创建谁回收,回收不了init进程负责。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
4.进程在内核本质
进程在内核中的形态就是task_struct,主要数据如下:
struct task_struct{
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
#endif
/* -1 unrunnable不可运行, 0 runnable可运行, >0 stopped已经停止: */
/* 进程状态TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE、TASK_STOPPED、TASK_TRACED */
volatile long state;
/* 进程标识符,相当于每一个学生的学号一样,标识符唯一标识进程 */
pid_t pid;
/* P所在进程组的领头进程的PID */
pid_t tgid;
/* Real parent process: */
/* 指向创建了P的进程的描述符,如果P的父进程不再存在,就指向进程1(init)的描述符(因此,如果用户运行一个后台进程而且退出了shell,后台进程就会成为init的子进程 */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
/* 指向P的当前父进程(这种进程的子进程终止时,必须向父进程发信号)。它的值通常与real_parent一致,但偶尔也可以不同,例如,当另一个进程发出监控P的ptrace()系统调用请求 */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural children:
*/
/* 链表的头部,链表中的所有元素都是P创建的子进程 */
struct list_head children;
/* 指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟进程的父进程都是P */
struct list_head sibling;
/* P所在进程组的领头进程的描述符指 */
struct task_struct *group_leader;
/* 用来表示进程与文件系统的联系,包括当前目录和根目录 Filesystem information: */
struct fs_struct *fs;
/* 表示进程当前打开的文件 Open file information: */
struct files_struct *files;
#ifdef CONFIG_NUMA
/* Protected by alloc_lock: */
struct mempolicy *mempolicy;
short il_prev;
short pref_node_fork;
#endif
}
5. exec家族
一段运行或是活的代码就是进程,其实这种说法是不精确的。更准确的说是系统先创建了进程,然后让进程去运行我们写得代码。只不过可执行文件在运行时系统先给我们创建了进程。可以使用exec模拟这一场景。
#include
#include
int main(int argc,char** argv) {
int pid = fork();
if (pid == 0) {
printf("I am child my pid = %d,my parent pid = %d\n",getpid(),getppid());
execv("./task",NULL);
} else {
wait(NULL);
}
return 0;
}
以上代码就是先创建一个进程,然后执行当前目录的task任务。
线程
使用pthread可以操作线程,对linux内核来说是不区分进程和线程的,它也是task_struct,只不过把内存、文件系统等资源设置成了共享的。当然进程比线程更重一些,更耗资源一些,其实linux的很多应用是使用进程实现,因为相对其它系统它本身就是轻量级的。这也是linux宣称的一切皆进程。
创建线程
#include
#include
#include
int a = 3;
void * thread_handle_fun(void * args) {
printf("thread_handle_fun a = %d\n",a);
printf("current thread = %d\n",pthread_self());
printf("my pid = %d\n",getpid());
a = 4;
printf("thread_handle_fun modify a\n");
return NULL;
}
int main(int argc,char** argv) {
pthread_t pt;
if (pthread_create(&pt,NULL,thread_handle_fun,NULL) == -1) {
perror("create error!");
return -1;
}
printf("main current thread = %ul\n",pthread_self());
printf("main my pid = %d\n",getpid());
if (pthread_join(pt,NULL)) {
perror("thread is not exit");
return -1;
}
printf("main a = %d\n",a);
return 0;
}
注:当一个线程正常退出对宿主进程没有影响,但是如果一个线程异常退出了,宿主进程也会跟着挂。这也是安全要求较高的应用大多使用进程,而不是线程的原因。
#include
#include
#include
void * thread_handle_fun(void * args) {
printf("thread_handle_fun modify a\n");
for(int i = 0;i < 5;i++) {
sleep(1);
printf("i = %d\n",i);
if (i ==3) {
pthread_exit(NULL);
//int *array = (int*) args;
//array[0] = 1;
}
}
return NULL;
}
int main(int argc,char** argv) {
pthread_t pt;
if (pthread_create(&pt,NULL,thread_handle_fun,NULL) == -1) {
perror("create error!");
return -1;
}
if (pthread_join(pt,NULL)) {
perror("thread is not exit");
return -1;
}
while(1) {
sleep(1);
}
return 0;
}
Linux文件
Linux 中所有内容都是以文件的形式保存和管理的,即一切皆文件,普通文件是文件,目录是文件,硬件设备(键盘、监视器、硬盘、打印机)是文件,就连套接字(socket)、网络通信等资源也都是文件。
Linux系统中,文件具体类型: 普通文件 、目录、字符设备和块设备(/dev)、套接字文件(socket)(/var/run/)、符号链接文件(symbolic link)、管道文件(pipe)。
1. 对文件的基本操作
libc操作代码
#include
#include
int main(int argc,char** argv) {
FILE * pfile = fopen("1.txt","w+");
char array[] = "this is a test of operating file";
char buffer[50];
fwrite(array,strlen(array) + 1,sizeof(char),pfile);
fseek(pfile,0,SEEK_SET);
fread(buffer,strlen(array) + 1,sizeof(char),pfile);
puts(buffer);
fclose(pfile);
return 0;
}
系统调用操作代码
#include
#include
#include
#include
#include
#include
int main(int argc,char** argv) {
int fd = open("1.txt",O_RDWR);
char str[] = "this is a test of operating file using system call.";
char buffer[50];
if (fd > 0) {
printf("sizeof(str) = %lu\n",sizeof(str));
printf("strlen(str) = %lu\n",strlen(str));
write(fd,str,sizeof(str));
lseek(fd,0,SEEK_SET);
read(fd,buffer,sizeof(str));
puts(buffer);
close(fd);
}
return 0;
}
一切皆文件kernel实现
一切皆文件的基本哲学是要对用户提供统一的操作界面或接口。内核的虚拟文件系统(VFS)提供此功能的支持。给用户空间的程序提供统一文件系统接口,给驱动程序提供统一的规约,允许不同的文件系统共存。
https://blog.csdn.net/mignatian/article/details/81673713
字符设置驱动
https://www.cnblogs.com/chen-farsight/p/6155518.html
推荐阅读
https://cloud.tencent.com/developer/article/1507511