美文网首页
Linux环境高级编程基础

Linux环境高级编程基础

作者: azmohan | 来源:发表于2020-08-11 14:06 被阅读0次

    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<stdio.h>
    #include<unistd.h>
    
    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. 进程运行状态机

    image.png
    • 运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

    • 就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由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<stdio.h>
    #include<unistd.h>
    
    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<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    
    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<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    
    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)。

    WeChat6cabeb442e842f871ee8b49a06eef941.png

    1. 对文件的基本操作
    libc操作代码

    #include<stdio.h>
    #include<string.h>
    
    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<stdio.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    #include<unistd.h>
    
    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

    WeChataa736f04effbf06bfffc88f19efb9c27.png

    字符设置驱动

    https://www.cnblogs.com/chen-farsight/p/6155518.html

    推荐阅读

    https://cloud.tencent.com/developer/article/1507511

    相关文章

      网友评论

          本文标题:Linux环境高级编程基础

          本文链接:https://www.haomeiwen.com/subject/wbzldktx.html