Linux

参考大佬专栏: Linux 教程 | 爱编程的大丙 及其B站视频

基础

文件IO

进程与线程

阿Q理解

操作系统(windows & Linux)区别

  • windows
    • 进程和线程的概念有明确定义,进程的概念对应于一个程序的运行实例(instance),而线程则是程序代码执行的最小单元
    • 提供API,CreateThread()用于建立一个新的线程,传递线程函数的入口地址和调用参数给新建的线程,然后新线程就开始执行了
    • 一个线程拥有自己的堆栈、寄存器(包括程序计数器PC,用于指向下一条应该执行的指令在内存中的位置),而代码段、数据段、打开文件这些进程级资源是同一进程内多个线程所共享的。因此同一进程的不同线程可以很方便的通过全局变量(数据段)进行通信
#include <windows.h>
#include <stdio.h>

// 全局变量(位于数据段),被所有线程共享(模拟进程级资源)
int sharedCounter = 0;
const int MAX_COUNT = 10;

// 线程函数声明
DWORD WINAPI IncrementThread(LPVOID lpParam);

int main() {
    HANDLE hThread;
    DWORD dwThreadId;

    printf("主线程: 进程ID=%d, 初始共享值=%d\n\n", GetCurrentProcessId(), sharedCounter);

    // 创建第一个线程(增加计数器)
    hThread = CreateThread(
        NULL,        // 默认安全属性
        0,           // 默认堆栈大小
        IncrementThread, // 线程函数
        NULL,        // 无参数传递
        0,           // 默认创建标志
        &dwThreadId // 返回线程ID
    );

    // 等待子线程完成
    WaitForSingleObject(hThread, INFINITE);

    // 关闭线程句柄
    CloseHandle(hThread);

    printf("\n主线程: 最终共享值=%d\n", sharedCounter);
    return 0;
}

// 增加共享计数器的线程函数
DWORD WINAPI IncrementThread(LPVOID lpParam) {
    printf("增量线程[ID=%d]: 开始增加共享计数器...\n", GetCurrentThreadId());
    
    for (int i = 0; i < MAX_COUNT; i++) {
        sharedCounter++; // 直接修改全局变量
        printf("增量线程: 设置 sharedCounter = %d\n", sharedCounter);
    }
    
    printf("增量线程: 完成增加操作\n");
    return 0;
}
  • Linux
    • 只有进程而无线程,然而它的进程又可以表现得像windows下的线程
    • linux利用fork()和exec函数族来操作多进程,fork()函数可以在进程执行的任何阶段被调用,一旦调用,当前进程就被叉分为父进程和子进程,两者拥有相同的代码段和暂时相同的数据段(虽然暂时相同,但从分叉开的时刻就是逻辑上的两个数据段了,之所以说是逻辑上的,是因为这里是“写时复制”机制,也就是,除非万不得已有一个进程对数据段进行了写操作,否则系统不去复制数据段,这样达到了负担最小),两者的区别在于fork()函数返回值,对于子进程来说返回为0,对于父进程来说返回的是子进程id,因此可以通过if(fork()==0)…else…来让父子进程执行不同的代码段,从而实现“分叉”
#include <stdio.h>
#include <unistd.h>
#include<sys/wait.h>

int main() {
    pid_t fpid; // fpid表示fork函数返回的值
    int count = 0;
    
    printf("父进程开始, 进程ID: %d\n", getpid());
    
    fpid = fork();
    
    if (fpid < 0) {
        printf("fork过程中发生错误!");
        return 1;
    } else if (fpid == 0) {
        // 子进程代码
        printf("我是子进程, 我的进程ID是: %d\n", getpid());
        printf("父进程ID是: %d\n", getppid());
        count++;
        
        // 子进程可以执行额外操作
        printf("子进程正在执行额外任务...\n");
        for (int i = 0; i < 3; i++) {
            printf("子进程计数: %d\n", i);
            sleep(1);
        }
        
    } else {
        // 父进程代码
        printf("我是父进程, 我的进程ID是: %d\n", getpid());
        printf("我创建的子进程ID是: %d\n", fpid);
        count++;
        
        // 父进程可以执行额外操作
        printf("父进程正在等待子进程完成...\n");
        // 父进程等待子进程结束
        wait(NULL);  // 可以使用waitpid()更精确的调控
        printf("子进程已完成,父进程继续执行\n");
        
        for (int i = 0; i < 3; i++) {
            printf("父进程计数: %d\n", i);
            sleep(1);
        }
    }
    
    // 两进程共用代码
    printf("进程 %d 的统计结果是: %d\n", getpid(), count);
    return 0;
}

对上述代码进行建议修改,可以简单模拟一台简易版的Linux

#include <stdio.h>
#include <unistd.h>
#include<sys/wait.h>

int main() {
    // main()可视为一台简易版的Linux
    // count 可看作进程向系统申请的共享内存
    int count = 0;

    int simulateMain(){
        // 模拟main()函数,可视为启动进程
        pid_t fpid; // fpid表示fork函数返回的值
        printf("父进程开始, 进程ID: %d\n", getpid());
    
        fpid = fork();
        
        if (fpid < 0) {
            printf("fork过程中发生错误!");
            return 1;
        } else if (fpid == 0) {
            // 子进程代码
            printf("我是子进程, 我的进程ID是: %d\n", getpid());
            printf("父进程ID是: %d\n", getppid());
            count++;
            
            // 子进程可以执行额外操作
            printf("子进程正在执行额外任务...\n");
            for (int i = 0; i < 3; i++) {
                printf("子进程计数: %d\n", i);
                sleep(1);
            }
            
        } else {
            // 父进程代码
            printf("我是父进程, 我的进程ID是: %d\n", getpid());
            printf("我创建的子进程ID是: %d\n", fpid);
            count++;
            
            // 父进程可以执行额外操作
            printf("父进程正在等待子进程完成...\n");
            // 父进程等待子进程结束
            wait(NULL);  // 可以使用waitpid()更精确的调控
            printf("子进程已完成,父进程继续执行\n");
            
            for (int i = 0; i < 3; i++) {
                printf("父进程计数: %d\n", i);
                sleep(1);
            }
        }
        
        // 两进程共用代码
        printf("进程 %d 的统计结果是: %d\n", getpid(), count);
    }

    simulateMain();

    return 0;
}
  • vfork()函数与fork()函数相同,都是系统调用函数,两者的区别是在创建子进程时,fork()函数会复制所有的父进程的资源,包括进程环境、内存资源等,而vfork()函数在创建子进程时不会复制父进程的所有资源,父子进程共享地址空间。这样,在子进程中对虚拟内存空间中变量的修改,实际上是在修改父进程虚拟内存空间中的值
    注意: 在使用vfork()函数时,父进程会被阻塞,需要在子进程中调用_exit()函数退出子进程,不能使用exit()退出函数
    在exit系统调用中,函数exit()在终止进程时会关闭所有文件,清空缓冲区。因此,如果在fork()函数和vfork()函数中使用exit()函数终止子进程,会清空标准输入/输出流,可能造成临时文件丢失,并且vfork()函数是父子进程共享虚拟内存,如果在子进程中使用exit()函数会严重影响到父进程,所以在使用这两个创建进程的函数时,尽量都不要使用exit()函数终止子进程
  • exec函数族的函数的作用则是启动另一个程序的新进程,然后完全用那个进程来代替自己(代码段被替换,数据段和堆栈被废弃,只保留原有进程id)
    例子:在shell中执行命令时(如ls),shell会先fork()一个子进程,然后在子进程中exec()执行ls程序
  • Linux进程不像windows线程那样方便通信,因为他们间无共享数据段、地址空间,他们间的通讯通过管道(无名管道用于父子进程通信, 命名管道用于任意两个进程间的通讯)、共享内存(一个进程向系统申请一块可以共享的内存, 其他进程通过标识符取到该内存, 并将其连接到自己的地址空间, 效果类似于windows下的多线程间的共享数据段、信号量、 信息队列、 套接字)

多线程

C

线程函数内容
#include<pthread.h>
// 线程函数
/*
    线程ID类型: pthread_t (X86_64位架构中, 大小为8字节, unsigned long long)

    函数:
        1、返回当前线程的线程ID pthread_self
        2、创建线程  int pthread_create(*thread, *attr(线程属性, 默认填NULL), void* (*start_routing)(void *), void *arg);
            备注:子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,
                如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。
                但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。
        3、线程退出  pthread_exit(void *retval)  retval携带线程退出时的数据
        4、线程回收(阻塞函数, 子线程运行函数对应的主线程就会被阻塞)
            int pthread_join(thread, void** retval) retval - 指向pthread_exit()传出数据的地址
        
        注意子线程与主线程间的信息传递过程
        子线程和主线程共用一个栈区,当子线程退出时其在栈区的内存就会被回收,因此exit()传出的数据最好用堆区数据
            (也可以全局变量保存 - 主线程栈,将数据传入arg中或者static关键字)保存       
        
        5、线程分离(不阻塞) int pthread_detach(thread)
            程序中的主线程有属于自己的业务处理流程,
            如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,
            主要线程的任务也就不能被执行了

            线程分离主线程不会被阻塞,仍会出现子线程还没执行完主线程执行完的情况,因此可以在主线程中设置退出时间

        6、线程取消 int pthread_cancel(thread)
            使用该函数杀死一个线程需要两步:
            1)、在主线程A中调用线程曲线函数,指定杀死子线程B(此时B不会被直接杀死)
            2)、在子线程B中执行一次系统调用(从用户区切换到内核区),否则子线程B会一直执行


        7、线程ID比较 int pthread_equal(pthread1, pthread2)
            在Linux中线程ID本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的ID,
            但是线程库是可以跨平台使用的,在某些平台上 pthread_t可能不是一个单纯的整形,
            这中情况下比较两个线程的ID必须要使用比较函数
*/
Q1:Linux中编译失败

注意:Compile and link with -pthread

gcc FILENAME –lpthread -o APPNAME

Q2:主线程和子线程间的信息接受

注意子线程中不要把局部变量返回,解决 – 应当设为全局变量或者在堆区创建数据

解决办法1全局变量

利用static关键字,申明变量为全局变量

或者:main函数外设置全局变量

解决办法2堆区创建数据

使用malloc在堆区创建数据

解决办法3 – 利用主函数栈

在主线程中创建变量,然后再将其地址传入到子线程中

#include<pthread.h>
#include<string.h>
#include<stdio.h>
#include<stdlib.h>

struct ThreadArgs{
    char *message;
    int number;
};

void* User1(void* args){
    printf("ChildThread ID: %ld, starting.....\n", pthread_self());
    struct ThreadArgs *Args = (struct ThreadArgs *)args;
    printf("MAIN SEND MESSAGE:\n%s\n", Args->message);

    Args->number = 2;
    // int* number = malloc(sizeof(int));
    // *number = 2;
    // static int number1 = 2;
    int *number = &Args->number;
    pthread_exit(number);
}

void* User2(void* args){
    printf("ChildThread ID: %ld, starting.....\n", pthread_self());
    struct ThreadArgs* Args = (struct ThreadArgs*) args;
    printf("MAIN MEND MESSAGE:\n%s\n", Args->message);
    Args->number = 6;

    // int* number = malloc(sizeof(int));
    // *number = 6;
    // static int number2 = 6;

    int *number = &Args->number;
    pthread_exit(number);
}

int main(){
    pthread_t U1;
    pthread_t U2;
    
    int number1, number2;

    char sendtoU1[] = "Hello, U1, are you ready to send me a number?\n";
    char sendtoU2[] = "Hello, U2, are you ready to send me a number?\n";

    struct ThreadArgs Args1 = {sendtoU1, number1};
    struct ThreadArgs Args2 = {sendtoU2, number2};


    pthread_create(&U1,NULL,User1, &Args1);
    pthread_create(&U2,NULL,User2,&Args2);

    // 接受User1和User2发来的信息
    void *U1message = NULL, *U2message = NULL;
    pthread_join(U1, &U1message);
    pthread_join(U2, &U2message);

    int *U1number = (int *)U1message;
    int *U2number = (int *)U2message;
    // 输出结果
    printf("U1 send Message: %d\nU2 send Message: %d\n", *U1number, *U2number);
    printf("Result: %d\n", *U1number + *U2number);

    // free(U1number);
    // free(U2number);

    printf("number1: %d\n", number1);
    printf("number2: %d\n", number2);
    return 0;
}
Q3:pthread_cancel 子线程退出时机

子线程B中执行一次系统调用(从用户区切换到内核区),否则子线程B会一直执行

或者设置显式cancel点

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

static void *new_thread_start(void *arg) {
    for(;;){
        printf("waiting.......\n");
        pthread_testcancel(); // 显式取消点
        sleep(1);
    }
    return (void *)0;
}

int main(void) {
    pthread_t tid;
    void *tret;
    int ret;

    // 创建新线程
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if(ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    sleep(1);

    // 向新线程发送取消请求
    ret = pthread_cancel(tid);
    if(ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }

    // 等待新线程终止
    ret = pthread_join(tid, &tret);
    if(ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (unsigned long long)tret);
    exit(0);
}

C++

#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<chrono>
using namespace std;

/*

1、构造函数
    · thread() noexcept;   - 构造线程对象,在该线程中不进行任何操作
    
    · thread(thread&& other) noexcept;   - 移动构造函数,将other线程的所有权转让给新的thread对象,other不再执行线程
    
    · thread(const thread& ) = delete;  - 显式删除拷贝构造,两个线程是无法共享栈空间的

    · // explicit关键字用于声明 构造函数 或类型转换运算符,​​阻止编译器执行非预期的隐式类型转换
      template<class Function, class... Args>
      explicit Thread( Function&& f, Args&& ... args);

      Thread t1 = {f, args}; 错误
      Thread t1(f, args);  正确,explicit表明只能显式构造

2、公共成员函数
    · get_id();   - 获取线程ID
    · join();     - 主线程主动等待子线程的终止(线程阻塞): join函数在哪个线程中执行,就阻塞哪个线程
    · detach();   - 线程分离; 在线程分离后就无法在主线程中对子线程做任何控制(get_id(), join()...)
    · joinable(); - 判断主线程和子线程是否关联
    · operator=; - 只能移动赋值(资源所有权转移), 而不能拷贝赋值

3、静态函数
    · hardware_concurrency(); - 返回CPU核心
        根据该结果创建出数量相等的线程,并发效率最高 (p: m / (n + m)  > i / (n + i)  -  i < m )
            m - cpu核心数, n - 除当前线程外正在运行的全部线程数
*/

// 模拟 thread 库
class SimulateThread{
    public:
        // 构造函数
        SimulateThread() noexcept;
          
        SimulateThread(SimulateThread&& other) noexcept;
        SimulateThread(const SimulateThread& ) = delete;


        // 可以直接从构造函数中引用传值,从而获取到子线程的相关数据
        template<class Function, class... Args>
        explicit SimulateThread(Function&& f, Args&& ... args){
            // 在此处启动新线程

            // 此处演示如何将 可变模板参数args传值于函数f中 (完美转发)
            // std::apply 的用处是将一个元组(std::tuple)展开,并将其元素作为参数传递给一个可调用对象
            auto _M_thread = [f = std::forward<Function>(f), 
                        args = std::make_tuple(std::forward<Args>(args)...)](){
                            std::apply(f, args);
                        };
            _M_thread();
        }

        // 公共成员函数
        // get_id() - 获取线程ID
        // this_thread::get_id()  - 获取当前线程ID
        std::thread::id get_id() const noexcept{
            return this_thread::get_id();
        };

        // join() - 阻塞主线程并能返回子线程中的数据,注意此处和C有区别!!! - 无法从join中获取子线程的返回值
        void join();

        // detach() - 线程分离
        void detach();

        // joinable()
        bool joinable() const noexcept;

        // operator=
        // 移动赋值
        SimulateThread& operator= (SimulateThread&& other) noexcept;

        // 禁止拷贝赋值
        SimulateThread& operator= (const SimulateThread& other) = delete;

        // 静态成员函数
        // hardware_concurrency() - 获取cpu核心数
        static unsigned int hardware_concurrency() noexcept;

};

mutex cout_mutex;

// 来点有趣的事情:模拟同时下载两个文件,都下载好后再进行一些额外操作 (显示总大小)
void download(const string& URL, int& SIZE){
    {
        // 由于多线程同时向标准输出写入,如果没有锁,输出的结果是非常混乱的(实际下载过程中无需互斥锁 - 抢时间片进行下载)
        lock_guard<mutex> lock(cout_mutex);
        cout << "ID: " << this_thread::get_id() << ", DOWNLOADING......" << "\tURL: " << URL <<endl;
    }

    SIZE = rand();

    // 模拟进度条
    for(int i = 0; i < SIZE; ++i){
        this_thread::sleep_for(chrono::milliseconds(100));
        if(i % (SIZE / 10) == 0)
            {
                lock_guard<mutex> lock(cout_mutex);
                cout << "ID: "<< this_thread::get_id() << "\tALREADY DOWNLOAD: " << (100 * i / SIZE) << "%" << endl;
            }
    }
}

// 下载完成后操作,返回总文件大小
void AfterDownload(int& SIZE1, int& SIZE2){
    cout << "DownLOAD COMPLETE ! " << endl << "TOTAL SIZE: " << SIZE1 + SIZE2 << "(G)";
}


int main(){
    int file1_size, file2_size;
    string url1 = "www.baidu.com";
    string url2 = "https://leetcode.com";
    thread t1([&](){
        download(url1, file1_size);
    });

    thread t2([&](){
        download(url2, file2_size);
    });

    t1.join();
    t2.join();

    AfterDownload(file1_size, file2_size);

    return 0;
}

进程同步

套接字通信(网络)

暂无评论

发送评论 编辑评论


				
上一篇
下一篇