Linux 多进程、多线程编程基础

    技术2022-07-13  83

    引言

    Linux 是多任务操作系统,可以同时运行多个进程,来完成多项工作。

    进程就是处于活动状态的程序,占用一定的内存空间。进程可以把自己复制一份,从而创造出一个新的进程。新的进程称为 子进程,原来的进程称为 父进程

    进程可以复制自己。这意味着启动一个程序,可能会产生多个进程。这样的程序能同时进行多项工作。多进程编程就是要设计一个这样的程序。

    进程的状态

    进程从创建到运行结束,经历的全部过程,称为进程的生命周期。在生命周期的不同阶段,进程会呈现不同的状态。下表列出了进程可能出现的所有状态。

    状态 含义 创建状态 : 正在创建 就绪 :刚刚创建好,还没运行过 内核状态 :运行中 用户状态 :暂停中 睡眠 :运行中的进程因为某些需求得不到满足而进入等待状态 唤醒 :睡眠中的进程,正在被唤醒 被抢占 :运行期间,CPU 被另一个进程抢占 僵死状态 :进程已经结束,但内存空间等占用的资源还未释放,被称为僵尸进程

    进程的管理

    每个进程都有一个唯一的编号,记为 PID (process id)。PID 应该用 pid_t 类型的变量来保存,pid_t 类型其实是 unsigned int 的别名。

    下面两个函数一个返回当前进程的 PID,一个返回父进程的 PID

    #include <unistd.h> pid_t getpid(void); pid_t getppid(void);

    在 Linux 系统中,除了 init 进程,其他进程都有一个父进程。对于在终端启动程序,它的父进程就是 Shell。下面程序在运行时输出自己的 PID 和父进程的 PID

    #include <stdio.h> #include <unistd.h> int main() { pid_t pid; pid_t ppid; pid = getpid(); ppid = getppid(); printf("pid: %d\n", pid); printf("ppid: %d\n", ppid); return 0; }

    fork() 复制进程

    fork() 函数以父进程为蓝本,产生一个子进程。

    #include <unistd.h> pid_t fork(void);

    对于 fork() 函数的返回值,如果创建失败,将得到 -1;如果创建成功,在父进程中将得到子进程的 PID,在子进程中将得到 0

    如果在 fork() 函数之后用一个 if 语句对 fork() 函数的返回值进行判断,子进程和父进程将进入不同的分支。

    下面程序中的 if 语句让父进程输出 parent hello!,而子进程输出 child hello!

    #include <stdio.h> #include <unistd.h> int main() { pid_t pid; pid = fork(); if (pid == -1) { perror("fork error"); return -1; } if (pid == 0) { printf("child hello!\n"); } else { printf("parent hello!\n"); } return 0; }

    sleep() 进入睡眠

    调用 sleep() 函数,进程将进入睡眠状态,传递给 sleep() 函数的参数就是睡眠的持续时间,单位秒。下面代码将使进程进入睡眠状态,持续 3 秒钟。

    #include <unistd.h> sleep(3);

    wait() 等待进程结束、exit() 结束进程

    父进程调用 wait(),将进入睡眠状态,直至子进程进入僵死状态,返回值是子进程的 PID。子进程调用 exit() 将使自己进入僵死状态。

    #include <sys/wait.h> // wait() #include <stdlib.h> // exit() pid_t wait(int * status); void exit(int status);

    这两个函数都只有一个参数。exit() 函数的参数是一个整型变量,用来保存一个范围在 0-255 之间的整数,最终这个整数的地址会保存在 wait() 函数的参数中。

    进程间通信

    信号机制是进程间的一种通信机制。信号是一系列整数,用来代表不同的事情发生。信号可以被进程捕获,捕获时会调用提前指定的信号处理函数,信号会作为参数传递。信号可以是由另一进程产生,也可由内核发出。

    实际上,子进程调用 exit() 时就会产生一个名为 SIGCHLD 的信号,父进程只要提前为这个信号指定处理函数就好,而不用专门调用 wait() 来等待。

    signal() 绑定信号处理函数

    调用 signal() 函数可以为某个信号指定处理函数。

    #include <signal.h> typedef void (* sighandler_t)(int); sighandler_t signal(int signum, sighandler_t sighandler);

    signal() 函数只接受两个参数,一个用于指定信号,一个用于给出信号的处理函数。信号处理函数必须是无返回值、只接受一个整数作为参数的函数。

    如果成功,signal() 函数的返回值是原本的信号处理函数,否则返回 SIG_ERR

    信号 SIGINT 是一个在前台运行的进程在用户按下 Ctrl + C 时可以捕获到的信号。下面程序将信号 SIGINT 的处理函数设为 sig_handler(),这个函数会输出 interrupted!,故用户按下 Ctrl + C 时会提示 interrupted!

    #include <stdio.h> #include <stdlib.h> #include <signal.h> void sig_handler(int sig) { printf("interrupted!\n"); exit(0); } int main() { if (signal(SIGINT, sig_handler) == SIG_ERR) { perror("signal error"); return -1; } while (1); }

    信号处理函数除了可以是自定义的函数,还可以使用 SIG_IGN 和 SIG_DFL 这两个宏。

    #include <bits/signum.h> #define SIG_IGN ((sighandler_t) 0) #define SIG_DFL ((sighandler_t) 1)

    SIG_IGN 表示忽略目标信号;SIG_DFL 表示采用信号的默认处理方式。

    处理僵尸进程

    子进程终止时,父进程可以捕获 SIGCHLD 信号。如果父进程不对 SIGCHLD 信号进行处理,子进程会继续占用资源而成为僵尸进程。

    最简单的解决办法是把 SIGCHLD 信号的处理函数指定为 SIG_IGN

    signal(SIGCHLD, SIG_IGN);

    kill()、raise() 发送信号

    进程可以用 signal() 函数给信号绑定信号处理函数,一旦信号到来,信号处理函数就会被调用。进程还可以用 kill() 函数给另一进程发送信号,或者用 raise() 函数给自己发送信号。

    #include <signal.h> int kill(pid_t pid, int sig); int raise(int sig);

    kill() 函数在成功时返回 0,失败时返回 -1,errno 的可能取值如下:

    EINVAL 无效的信号 EPERM 无权限发送信号 ESRCH 目标不存在 下面程序创建的子进程在给信号 SIGINT 绑定处理函数后进入死循环,父进程在睡眠 3 秒后向子进程发送信号 SIGINT,使得子进程中的信号处理函数被调用,最终输出 interrupted!

    #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> void sig_handler(int sig) { printf("interrupted!\n"); exit(0); } int main() { pid_t pid; pid = fork(); if (pid == -1) { perror("fork error"); return -1; } if (pid == 0) { if (signal(SIGINT, sig_handler) == SIG_ERR) { perror("signal error"); return -1; } while (1); } else { sleep(3); kill(pid, SIGINT); } return 0; }

    多线程

    进程进一步细分,就是线程。每一个进程都至少有一个线程,这个线程称为 主线程。主线程就是运行主函数 main() 的线程。创建线程相当于调用一个函数,只不过原来的线程会立即执行后续的代码而不等待这个函数返回。这使得被调函数中的代码和后续的代码是并行执行的。因此,可以简单地认为多线程就是同时运行多个函数。

    历史上曾出现过多种线程标准。这些标准互不兼容,这使得程序员难以开发可移植的应用程序。为此,IEEE 制订了后来被广泛采用的线程标准 POSIX threads,简称 Pthreads。POSIX 线程库 实现了这个标准。POSIX 线程库也是最常用的线程库。使用 POSIX 线程库需要包含头文件 pthread.h

    #include <pthread.h>

    由于 POSIX 线程库并不属于默认库,因此在使用 gcc 命令进行编译时,要加上 -lpthread 选项。

    pthread_create() 创建线程

    线程通过调用 pthread_create() 函数创建。

    int pthread_create( pthread_t * id, pthread_attr_t * attr, void * (* start_routine)(void *), void * arg );

    第一个参数要求一个 pthread_t 变量的地址。这个变量用来保存线程的标识符 第二个参数要求一个 pthread_attr_t 结构的地址。这个结构用于设定线程的一些属性,一般设为 0 第三个参数要求一个函数。创建的线程将调用这个函数。这个函数称为 线程函数。线程函数必须以一个 void 指针为参数,返回值也必须是一个 void 指针。 第四个参数是一个 void 指针,它将会作为线程函数的参数。如果不需要传参,设为 0 如果线程创建成功,pthread_create() 函数将返回 0,否则返回要给错误代码。这些错误代码是线程库定义的一些常量,但没有一个是 -1

    pthread_exit() 结束线程

    线程调用 pthread_exit() 函数可结束自己,这个函数相当于结束进程的 exit()

    void pthread_exit(void * retval);

    唯一的参数是一个 void 指针,用来指向返回值。

    pthread_join() 等待线程结束

    可以调用 pthread_join() 函数来等待另一个线程结束。

    int pthread_join(pthread_t id, void ** retval);

    第一个参数要求一个线程的标识符 第二个参数要求一个 void 指针的地址。这个指针将被指向线程的返回值。如果不需要得到线程的返回值,可设为 0 如果顺利,pthread_join() 函数将返回 0,否则返回一个错误代码。

    下面是一个完整的例子。演示了从创建线程到结束线程的过程。

    #include <stdio.h> #include <stdlib.h> // exit() #include <unistd.h> // fork(), sleep(), _exit(), pid_t #include <pthread.h> void * hello(void * arg) { printf("Thread start running!\n"); printf("%s\n", (char *)arg); sleep(3); pthread_exit("Hello from thread!"); } int main(void) { pthread_t id; void * thread_retval; if (pthread_create(&id, 0, hello, "Hello from main!") != 0) { printf("Create thread failed!\n"); exit(1); } pthread_join(id, &thread_retval); printf("%s\n", (char *)thread_retval); return 0; }

    pthread_detach() 脱离同步

    pthread_detach() 函数用来使一个线程与其他线程脱离同步。脱离同步是指其他线程不能用 pthread_join() 函数来等待这个线程结束。这个线程将在退出时自行释放所占的资源。

    int pthread_detach(pthread_t id);

    pthread_detach() 函数唯一的参数就是需要脱离同步的线程的标识符。如果顺利,将返回 0,否则返回一个错误代码。

    引用地址: https://www.jianshu.com/p/ef1cf8609c4a

    Processed: 0.024, SQL: 9