标签 unix环境编程 下的文章

做代码优化,发现代码中获取系统CPU核数是通过system调用命令得到的,想想最近被system支配的恐惧,果断改掉。

linux c中获取CPU核数的函数原语有两个:

#include <sys/sysinfo.h>
get_nprocs_conf();
get_nprocs();

第二个函数是返回当前可用的CPU数量,不可用的意思是CPU HANG住了。如果安装了man page,可以直接在man page中找到用法。

#include <stdio.h>
#include <sys/sysinfo.h>

int
main(int argc, char *argv[])
{
   printf("This system has %d processors configured and "
           "%d processors available.\n",
           get_nprocs_conf(), get_nprocs());
   return 0;
}

一、匿名管道

pipe的基本描述和用法

匿名管道是linux中的一种通信方式,为有血缘关系的进程提供数据通信。相关的api函数为:

#include <unistd.h>
int pipe(int pipefd[2]);

pipe函数传入一个长度为2的int数组,执行成功将在内核区域分配一块内存区域,并设置pipefd为管道的文件描述符。

创建子进程后,子进程也会继承这两个描述符,对于同一个文件描述符来说,父端写,子端就可以读,字段写,父端也可以读。

工作流程可以描述为:

但是由于管道内部实现是用的环形队列,因此父子不能同时对一个管道进行读操作或者写操作,队列是单向的,两端写就有问题。

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

#define SEND_BUFF "HelloWorld"

#define CLOSE_FD(fd) \
    do {    \
        if (fd != -1) { \
            close(fd);  \
            fd = -1;    \
        }   \
    } while (0)

int main(){
    int fd[2], ret, n;
    char buf[1025];

    ret = pipe(fd);
    if (ret == -1)
        goto _err;

    memset(buf, 0, 1025);
    pid_t pid = fork();

    if (pid == -1) {
        goto _err;
    } else if (pid == 0) {
        // 子端关闭fd1,打开fd0读
        CLOSE_FD(fd[1]);
        n = read(fd[0], buf, 1024);
        if (-1 == n) goto _err;
        printf("child read %d byte data: %s\n", n, buf);
        CLOSE_FD(fd[0]);
    } else {
        // 父端关闭fd0,打开fd1写
        CLOSE_FD(fd[0]);
        n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
        if (n == -1) goto _err;
        printf("parent send %d bytes data: %s\n", 
                sizeof(SEND_BUFF), SEND_BUFF);
        // 等待子进程退出
        wait(0);
        CLOSE_FD(fd[1]);
    }
    return 0;

_err:
    perror("Error");
    CLOSE_FD(fd[0]);
    CLOSE_FD(fd[1]);
    return -1;
}

1.2 非阻塞pipe

管道还有一个函数pipe2提供了更高级的选项,可以使得管道成为非阻塞读:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int pipe2(int pipefd[2], int flags);

其中,当flags置为O_NONBLOCK时,管道为非阻塞,从非阻塞的管道读取数据时,如果此时没有数据可读,将返回-1,并设置errno为EAGAIN

修改子进程部分代码为:

if (pid == 0){
    CLOSE_FD(fd[1]);
    while (1) {
        n = read(fd[0], buf, 1024);
        if (-1 == n) {
            if (errno == EAGAIN) {
                printf("READ AGAIN\n");
                // 没有数据可读时,休眠一秒,继续读
                sleep(1);
                continue;
            }
            goto _err;
        }
        printf("child read %d byte data: %s\n", n, buf);
        break;
    }
    CLOSE_FD(fd[0]);
}

父进程在写数据前先睡眠5秒:

else {
    CLOSE_FD(fd[0]);
    sleep(5);
    n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
    // ...
}

运行结果:

1.3 管道数据读取的原则

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭,这时有进程指向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
  4. 如果有指向管道读端的文件描述符没关闭,而持有管道写端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再write会阻塞,直到管道中有空位置了才写入数据并返回。

二、有名管道fifo

2.1 fifo的基本用法

fifo管道是linux文件系统中的一种文件类型,创建在磁盘上,任何进程都可以打开这个文件进行读写。

它可以解决无血缘关系进程的通信问题,创建管道的函数为:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

当管道文件被创建之后,就可像文件一样打开它进行读写:

// read.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include <errno.h>
#include<unistd.h>

#define FIFO_FILE "fifo"
#define BUFF_SIZE 1024

int main() {
    int ret, fd = -1;
    char buf[BUFF_SIZE] = { 0 };

    ret = mkfifo(FIFO_FILE, 0755);
    if (ret == -1 && errno != EEXIST)
        goto _err;

    fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1)
        goto _err;

    bzero(buf, BUFF_SIZE);
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1)
        goto _err;

    printf("read: %s\n", buf);
    close(fd);
    fd = -1;
    
    return 0;
_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

写管道程序:

// write.c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

#define FIFO_FILE "fifo"

int main(){
    int ret, fd;

    fd = open(FIFO_FILE, O_WRONLY);
    if (fd == -1)
        goto _err;
        
    unlink(FIFO_FILE);

    ret = write(fd, "HelloWorld", 11);
    if (ret == -1)
        goto _err;

    close(fd);
    fd = -1;

    return 0;

_err:
    perror("Error");
    if (fd > 0) {
        close(fd);
        fd = -1;
    }
    return -1;
}

此时运行./read,会创建管道文件,新开一个窗口就可以看到,管道的颜色和普通文件不一样,并且ls的第一个字符是p

默认情况下,读是阻塞的(类似于读文件),直到有数据过来为止,所以./read执行后,会阻塞,直到运行./write之后才会收到数据。

read: HelloWorld

2.2 非阻塞pipe

非阻塞pipe有一个注意事项:当打开的管道没有其他进程也打开的话(即只有当前进程在读,没有其他进程了),直接返回0,不会返回EAGAGIN

// 加上O_NONBLOCK标记
fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK);
if (fd == -1)
    goto _err;

bzero(buf, BUFF_SIZE);
printf("fd = %d\n", fd);
while (1) {
    ret = read(fd, buf, sizeof(buf) - 1);
    if (ret == -1) {
        if (errno == EAGAIN) {
            printf("READ AGAIN\n");
            sleep(1);
            continue;
        }
        goto _err;
    } else if (ret == 0) {
        printf("read end\n");
        close(fd);
        fd = -1;
        break;
    } else {
        printf("read: %s\n", buf);
    }
}

编译后如果直接执行,程序会直接退出,因为此刻没有其他进程在写,不会返回EAGAIN

一、线程同步的几种方法

多线程主要有以下几种同步方法:

  1. 互斥量
  2. 读写锁
  3. 屏障
  4. 条件变量
  5. 信号量
  6. 自旋锁

二、几种同步方式的比较

同步方式优缺点和适用范围
互斥量最简单的锁,使用临界区的方式锁住代码块,最基础的锁,适用于多线程互斥
读写锁有更高的并行性,只会锁住写锁,读锁共享,适用于读远大于写的场景
屏障适用于线程等待,直到多个线程到达同一点时再继续运行,主要用于同步
条件变量允许线程以无竞争的方式等待特定条件的发生,适用于生产者和消费者场景
信号量通过PV变量的方式,多用于生产者和消费者场景,和条件变量类似
自旋锁被锁住时线程处于忙等,适用于锁被持有的时间短,不希望线程把时间花在重新调度上

三、信号量和条件变量的区别

  1. 条件变量需要用到互斥锁,信号量不用
  2. 信号量是基于PV操作,需要多个信号量配合使用
  3. 信号量可以用于线程和进程,条件变量只能用于线程

一、线程的基本使用

正常情况下,一个进程只有一个一线程在运行。有了多线程之后,就可以使得程序在同一时间可以做多间不同的事情。

这样就可以利用起来很多被浪费掉的时间,例如执行阻塞任务,磁盘IO等等,这些等待的时间都可以被用来做其他的事情,这也就是多线程的用武之地。

1.1 线程标识

线程通过线程ID来标识,在linux环境中被定义为pthread_t类型。在调试中,我们可以把它当作一个整形变量打印,但是实际上在严谨的场合不能这么干,它并不等于整形。

判断两个线程id是否相等要使用linux提供的函数接口:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

对线程而言,可以通过pthread_self()函数来获得当前线程的ID,例如打印出一个函数中主线程的id:

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

int main() {
    pthread_t tid;

    tid = pthread_self();

    printf("main thread: %08x\n", tid);

    return 0;
}

编译运行:

main thread: 0x66744700
注意,多线程程序编译时要加-lpthread选项

1.2 创建一个函数

创建一个线程的函数原型:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                  void *(*start_routine) (void *), void *arg);

各个参数的含义:

  • thread:用来接收新创建的线程ID,该参数不能省略,不能写成NULL
  • attr:设置线程的属性,一般为NULL
  • start_routine:线程启动的函数,是一个函数指针,线程启动后将调用这个函数
  • arg:自定义参数,会作为线程函数的启动参数传入

以下是一个简单的创建线程的示例,创建一个线程并打印出它当前的进程id和线程id:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>

typedef unsigned int uint;

void f(const char *s) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    // 打印出当前的进程id和线程id
    printf("%s pid is %d, tid is 0x%x\n", s, pid, (uint)tid);
}

void *thread_start(void *arg) {
    f((char*)arg);
    return 0;
}

int main(){
    int err;
    pthread_t tid;
    // 创建线程
    err = pthread_create(&tid, 0, thread_start, (void*)"new thread:");
    if (err != 0) {
        fprintf(stderr, "pthread_create error: %s", strerror(err));
        return 0;
    }
    printf("main thread: pid is %d, child thread is 0x%x.\n", (int)getpid(), (uint)tid);
    // 等待线程执行完毕
    sleep(1);
    return 0;
}

运行结果:

main thread: pid is 18267, child thread is 0xe33ba700.
new thread: pid is 18267, tid is 0xe33ba700

根据两个线程打印出来的pid可以看到:多线程实际上还是处于同一个进程的。

二、线程终止

2.1 pthread_exit()

pthread_exit()的作用就是退出当前线程,并设置退出的返回值。定义为:

#include <pthread.h>
void pthread_exit(void *retval);

retval参数表示线程的退出状态。

线程也可以使用return来返回,但是在线程定义了清理函数的时候,如果使用return返回,清理函数将不会被执行。

2.2 pthread_join()

上面代码的主函数中,最后面有一个sleep(1)来等待子线程退出,这么做实际上是不合理的,因为线程和进程一样,执行的时机是不确定的,1S之后线程可能还没有执行完。

如果要在主线程等待子线程执行完毕的话,可以通过pthread_join()函数来完成,其定义为:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

参数含义:

  • thread:线程id
  • retval:线程的返回码

调用这个函数后,线程会一直阻塞,直到指定线程退出。如果调用时线程已经处于结束状态,那么函数会立即返回。

如果指定的线程被取消了,retval的值将会是PTHREAD_CANCELED。不关心线程返回值的话,可以直接把retval设为NULL。

注意,如果线程被分离了(执行了pthread_detach()),执行pthread_join()无效。

以下是一个调用pthread_exit()pthread_join()的示例:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>

typedef unsigned int uint;

void *func1(void *arg) {
    pthread_t tid;
    tid = pthread_self();

    printf("this is func1(), tid = 0x%08x\n", tid);
    return (void *)111;
}

void *func2(void *arg) {
    pthread_t tid;
    tid = pthread_self();

    printf("this is func2(), tid = 0x%08x\n", tid);
    pthread_exit((void *)222);
}

// 创建一个线程,并打印出对应的信息
int pthread_create_ex(pthread_t *tid, void *(pf)(void*)) {
    int err;

    err = pthread_create(tid, 0, pf, NULL);
    if (err != 0) {
        fprintf(stderr, "pthread_create error: %s", strerror(err));
    } else {
        printf("main thread: child thread is 0x%x\n", tid);
    }

    return err;
}

// 回收一个线程,并打印相关信息
int pthread_join_ex(pthread_t tid) {
    int err, retval;

    err = pthread_join(tid, (void *)&retval);
    if (err != 0) {
        fprintf(stderr, "pthread_join error: %s", strerror(err));
    } else {
        printf("thread 0x%x has exit, code %d\n", tid, retval);
    }

    return err;
}

int main(){
    int err; 
    pthread_t tid1, tid2;

    err = pthread_create_ex(&tid1, func1);
    if (err != 0)
        return -1;

    err = pthread_create_ex(&tid2, func2);
    if (err != 0)
        return -1;

    err = pthread_join_ex(tid1); 
    if (err != 0)
        return -1;

    err = pthread_join_ex(tid2);
    if (err != 0)
        return -1;
    
    return 0;
}

这里的代码用两个函数作为不同的线程开始,分别通过returnpthread_exit来返回,运行结果:

可以看到,pthread_join()都能拿到返回值,那这两者是否有区别呢?下面将会继续探究。

2.2 pthread_cancel()

pthread_cancel()函数可以取消同一进程中的其他线程:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数就是希望取消的线程id,这会使得被取消的线程返回PTHREAD_CANCELED

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

void *f1(void *arg) {
    int i = 0;
    while (i++ < 5) {
        printf("second %d\n", i);
        sleep(1);
    }
    pthread_exit((void *)99);
}

void *f2(void *arg) {
    pthread_t tid;

    tid = *(pthread_t *)arg;

    sleep(1);
    printf("pthread_cancel --> 0x%08x\n", tid);
    pthread_cancel(tid);
    pthread_exit((void *)0);
}

int main() {
    pthread_t tid1, tid2;
    pthread_t ret1, ret2;

    pthread_create(&tid1, NULL, f1, NULL);
    pthread_create(&tid2, NULL, f2, (void *)&tid1);

    pthread_join(tid1, (void *)&ret1);
    pthread_join(tid2, (void *)&ret2);

    printf("tid1: %08x, exit status: %d\n", tid1, ret1);
    printf("tid2: %08x, exit status: %d\n", tid2, ret2);
    printf("PTHREAD_CANCELED = %d\n", PTHREAD_CANCELED);

    return 0;
}

以上是一个简单的调用示例,产生两个线程,一个线程每隔一秒打印一次,另一个线程则在一秒后关闭该线程。

主线程中等待退出并打印返回码,结果如下所示:

被取消掉的线程91ef7700并没有返回其函数内定义的99,而是-1,也就是PTHREAD_CANCELED的值。

一、互斥量

互斥量的本身是一把锁,在访问共享资源时前对互斥量加锁,访问完成后释放。在对互斥量加锁后,其他线程如果想再次加锁,操作会被阻塞,直到锁被释放为止。

创建互斥量和销毁互斥量的函数定义:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
      const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

也可以直接使用宏定义PTHREAD_MUTEX_INITIALIZER完成对锁的初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

对互斥量加锁和解锁:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中,pthread_mutex_trylock()会尝试对互斥量加锁,如果此时互斥量没有被锁住,加锁成功。如果互斥量已经被其他线程锁住了,返回EBUSY。还有一个可以执行超时加锁的函数:

#include <pthread.h>
#include <time.h>

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
        const struct timespec *restrict abs_timeout);

对加锁设置超时时间,如果在指定时间内,加锁不成功(即锁还没有被释放或者释放了又被其他线程抢占了),加锁失败,直接返回,不阻塞线程了。

二、使用示例

分别创建50个线程,每个线程对全局变量n执行1000次自加操作:

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 线程函数
void *f(void *args) {
    int idx = 0;
    // n自加1000次
    while (idx++ < 1000) {
        pthread_mutex_lock(&mutex);
        n++;
        pthread_mutex_unlock(&mutex);
    }
    return 0;
}

int main() {
    int i;
    pthread_t ts[50];
    // 创建50个线程
    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    // 回收线程
    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }

    pthread_mutex_destroy(&mutex);
    info("n = %d\n", n);

    return 0;
}

执行结果:

mutex.png

三、死锁

当多个线程之间,对多个锁产生不同的竞争关系时,就有可能发生死锁现象。

注意:死锁只会发生在一个线程试图锁住另一个线程以相反顺序锁住的互斥量

例如此时有A/B两个锁,t1t2两个线程分别执行以下抢占:

  1. t1对A加锁,t2对B加锁,两者都加锁成功
  2. t2再对A加锁,t1对B加锁,此刻t1会等待t2释放B锁,而t2又要等待t1先释放A锁,此时产生死锁。

一、读写锁

读写锁和互斥量相似,都是对共享空间执行加锁和解锁的过程。不过,读写锁比互斥量有更高的并行性。

读写锁有三种状态:读模式加锁、写模式加锁和不加锁。读写锁只允许同时有一个线程占有写模式的锁。

这就意味着读锁可以同时被多个线程拥有,读写锁特别适合读次数远大于写次数的场景。

1.1 创建并初始化一把锁

读写锁的数据类型为pthread_rwlock_t,使用以下方式初始化和销毁:

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
      const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

1.2 加锁和解锁

#include <pthread.h>

// 加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock()可以同时对一把锁使用,不会阻塞线程。

1.3 带超时的读写锁

#include <pthread.h>
#include <time.h>

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
      const struct timespec *restrict abs_timeou
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
      const struct timespec *restrict abs_timeout);

二、示例

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_rwlock_t rwlock;

// 线程函数
void *f(void *args) {
    int idx = 0;
    while (idx++ < 1000) {
        pthread_rwlock_wrlock(&rwlock);
        n++;
        pthread_rwlock_unlock(&rwlock);
    }
    return 0;
}

int main() {
    int i;
    pthread_t ts[50];

    pthread_rwlock_init(&rwlock, NULL);

    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }

    pthread_rwlock_destroy(&rwlock);
    info("n = %d", n);

    return 0;
}

执行结果:

rwlock.png

一、自旋锁

自旋锁和互斥量相似,通过加锁和解锁来保护对共享数据的保护。

和互斥量不同的是:互斥量在锁已经被占有的情况下,会阻塞等待,此时线程处于休眠状态,不占用CPU资源。而自旋锁此时虽然也会被阻塞,但是不会休眠,处于忙等的状态,不会交出CPU资源。

自旋锁适用于那些锁持有时间短不希望把时间浪费在CPU调度上的场景。

linux内核中,自旋锁被广泛的使用,因为内核模块特别是在网卡驱动中,时间精度要求很高,相比其他锁切换时间,自旋锁的忙等效率更高一些。

自旋锁的数据类型为pthread_spinlock_t,相关函数为:

#include <pthread.h>

// 初始化和销毁自旋锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

// 对自旋锁加锁和解锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

自旋锁的函数定义基本和其他锁一致,具体使用方法不再详细描述。

二、示例

#include <stdio.h>
#include <pthread.h>
#include "log.h"

static int n = 0;

pthread_spinlock_t spin_lock;

static void *f(void *args) {
    int idx = 0;
    while (idx++ < 1000) {
        pthread_spin_lock(&spin_lock);
        n++;
        pthread_spin_unlock(&spin_lock);
    }
    return 0;
}

int main() {
    int i;
    pthread_spin_init(&spin_lock, 0);

    pthread_t ts[50];
    for (i = 0; i < 50; i++) {
        pthread_create(&ts[i], 0, f, 0);
    }

    for (i = 0; i < 50; i++) {
        pthread_join(ts[i], 0);
    }
    pthread_spin_destroy(&spin_lock);

    info("n = %d", n);

    return 0;
}

编译运行:

 一、条件变量

条件变量是多线程的一种同步方式,它允许多个线程以无竞争的方式等待特定事件发生。无竞争的意思是,当条件满足时,条件满足这个讯号会发送给所有的监听者线程,但多个线程中只有一个能获取到特定事件。条件变量需要配合互斥量一起使用。

相关数据结构和函数:

#include <pthread.h>

// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
       const pthread_condattr_t *restrict attr);
// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

条件变量的数据结构为pthread_cond_t,和其他同步的数据结构一样,也提供了两种方式来初始化。一种是通过赋值的方式,一种是通过函数的方式。在使用pthread_cond_init对初始化条件变量的时候,attr参数一般为NULL。

对条件变量加锁的方式:

#include <pthread.h>

// 等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);
// 带有超时条件的等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);

在对条件变量加锁的时候,需要传入一把已经处于加锁状态的互斥量,此时函数会把当前线程放到条件等待的列表上并对互斥量解锁。此时线程进入条件等待状态,当有信号到达时便会触发。

通知条件满足的函数:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal函数会唤醒条件等待队列上的至少一个线程,pthread_cond_broadcast会唤醒条件队列上的所有线程。

条件队列一般用于消息队列,用于统一、协调多个生产者和消费者之间的竞争关系。

二、使用示例

以下使用了三个线程作为消费者,分别从全局队列上获取消息消费:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include "log.h"

#define THREAD_COUNT 3

// 消息结构
struct msg_st {
    unsigned int msg_id;
    struct msg_st *next;
};

static struct msg_st *msg_queue = NULL;
static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;

// 消费线程函数
void *handle_msg(void *arg) {
    struct msg_st *msg;
    while (1) {
        pthread_mutex_lock(&g_mutex);
        while (msg_queue == NULL) {
            pthread_cond_wait(&g_cond, &g_mutex);
        }

        // 提取消息
        msg = msg_queue;
        msg_queue = msg_queue->next;
        pthread_mutex_unlock(&g_mutex);

        // 退出
        if (msg->msg_id == (unsigned int) -1) {
            info("Thread 0x%x exit!", (unsigned int) pthread_self());
            break;
        }

        info("Thread 0x%x: msg_id = %u", (unsigned int) pthread_self(), msg->msg_id);
    }
    return NULL;
}

// 生产消息函数
void create_msg(struct msg_st *msg) {
    pthread_mutex_lock(&g_mutex);
    msg->next = msg_queue;
    msg_queue = msg;
    pthread_mutex_unlock(&g_mutex);
    pthread_cond_signal(&g_cond);
}

int main() {
    int i;
    struct msg_st msg[10];
    pthread_t pid[THREAD_COUNT];

    debug("start create threads");
    // 创建3个线程作为消费者
    for (i = 0; i < 3; i++) {
        pthread_create(&pid[i], NULL, handle_msg, NULL);
    }

    debug("start create msgs");
    // 生产10个消息
    for (i = 0; i < 10; i++) {
        msg[i].msg_id = i + 1;
        msg[i].next = NULL;
        create_msg(&msg[i]);
    }

    // 休眠1秒,确保所有消息都被消费者消费完成
    sleep(1);

    // 退出所有线程
    debug("start create exit msgs");
    for (i = 0; i < THREAD_COUNT; i++) {
        msg[i].msg_id = (unsigned int) -1;
        msg[i].next = NULL;
        create_msg(&msg[i]);
    }

    // 回收所有线程
    debug("start join threads");
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(pid[i], NULL);
    }

    debug("program end");

    return 0;
}

运行结果,10个消息分别被3个线程打印出来了:

三、其他

3.1 “惊群”效应

条件变量是否存在惊群效应呢?

不会,线程执行wait操作的时候会被放到一个条件等待队列里面去。当条件满足的时候,系统会自动选择队列前面的线程来消费队列。

3.2 为什么wait前要加锁,这样不会死锁吗?

不会,执行wait操作时需要的是一把已经加锁的互斥量,这个锁在wait函数中会解开。

这样做的目的:如果在wait前没有加锁,生产者线程产生了一个消息并发送信号,再执行wait后信号就丢失了。

3.3 生产者的信号为什么放在unlock之后?

wait收到信号之后要对互斥量加锁,此时锁还被生产者持有,消费者依旧还要等待锁的释放,才能持有锁。对消费者而言,最理想的情况就是生产者产生消息后,我立马就能读取消息,此时的互斥量是用来和其他消费者线程同步的,而不是和生产者线程同步。

setuid、setgid、以及黏着位

setuid的作用是以该命令拥有者的权限去执行,比如修改密码的passwd命令,执行passwd时会拥有root权限,不然就修改不了/etc/passwd文件了。

而setgid的意思是以命令所有组的权限去执行,它们的标志位是s,出现在x的地方,例如-rwsr-xr-x

手动添加这一位的方式:

> touch ls
> chmod u+s ls # UID权限设置
> ll ls
-rwSrw-r--. 1 ma ma 0 Nov  3 00:50 ls
> chmod g+s ls # GID权限设置
> ll ls
-rwSrwSr--. 1 ma ma 0 Nov  3 00:50 ls

对于以下程序:

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

int main() {
    // 打印实际用户ID和有效用户ID
    printf("uid: %u euid: %d\n", getuid(), geteuid());
    return 0;
}

编译后加上UID权限,分别使用不同的身份运行:

> make setuid
gcc setuid.c -o debug/setuid
> ll debug/setuid 
-rwxrwxr-x. 1 ma ma 8552 Nov  3 02:24 debug/setuid
> chmod u+s debug/setuid # 加上UID权限位
> ll debug/setuid 
-rwsrwxr-x. 1 ma ma 8552 Nov  3 02:24 debug/setuid 
> sudo ./debug/setuid 
[sudo] password for ma: 
uid: 0 euid: 1000

可以看到,不管是以当前用户还是root用户运行,实际用户id都是一样的,都是其所有者。

二、黏着位(sticky bit)

关于黏着位找了很多资料也没有找到明确的描述,网上众说纷纭也没有清晰的描述出它的作用,最后还是在《UNIX环境高级编程》中找到一些更明确的解释:

The S_ISVTX bit has an interesting history. Onversions of the UNIX System that predated demand paging, this bit was known as the sticky bit.If it was set for an executable program file, then the first time the program was executed, a copy of the program’s text was saved in the swap area when the process terminated. (The text portion of a program is the machine instructions.) The program would then load into memory morequickly the next time it was executed, because the swap area was handled as a contiguous file, as compared to the possibly random location of data blocks in a normal UNIX file system. The sticky bit was often set for common application programs, such as the text editor and the passes of the C compiler. Naturally,therewas a limit to the number of sticky files that could be contained in the swap area beforerunning out of swap space, but it was a useful technique. The name sticky came about because the text portion of the file stuck around in the swap area until the system was rebooted. Later versions of the UNIX System referred to this as the saved-text bit; hence the constant S_ISVTX.With today’s newer UNIX systems, most of which have a virtual memory system and a faster file system, the need for this technique has disappeared.

在早期的unix系统中,如果一个程序被设置了黏着位,那么当它第一次执行结束之后,程序的正文段会被写入到交换空间中,以此加快后续使用的加载速度。因为交换空间是顺序存放,而磁盘上是随机的。它通常被设置成经常被使用的公用程序例如文本编辑器、编译器等,会一直持续到系统重启。

在后续的unix系统都设计了新的更快速的文件系统,所以这种用法逐渐消失。而对于这一黏着位“现在的用途”的描述是:

On contemporary systems, the use of the sticky bit has been extended. The Single UNIX Specification allows the sticky bit to be set for a directory. If the bit is set for a directory, a file in the directory can be removed or renamed only if the user has write permission for the directory and meets one of the following criteria:

  1. Owns the file
  2. Owns the directory
  3. Is the superuser

The directories /tmp and /var/tmp are typical candidates for the sticky bit—they are directories in which any user can typically create files. The permissions for these two directories are often read, write, and execute for everyone (user, group, and other). But users should not be able to delete or rename files owned by others.

现在的系统里面,黏着位已经被扩展了,它被用于目录权限上,如果一个目录设置了这一位,这个目录下的文件就只能被满足以下条件的用户重命名或者删除:

  1. 所有者
  2. 当前目录苏有这
  3. 超级用户

目录/tmp和/var/tmp是设置粘住位的候选者—这两个目录是任何用户都可在其中创建文件的目录。这两个目录对任一用户 (用户、组和其他)的许可权通常都是读、写和执行。但是用户不应能删除或更名属于其他人的文件,为此在这两个目录的文件方式中都设置了粘住位

手动给目录添加sticky位:

> mkdir 123
> chmod +t 123
> ll -d 123
drwxrwxr-t. 2 ma ma 6 Nov  3 02:18 123

加上这一个权限之后目录的颜色也有变化:

image

linux c获取文件路径和文件名

linux提供了两个函数分别用来获取文件所在的目录和文件名:

char *dirname(char *path);
char *basename(char *path);

它们被包含在头文件libgen.h中,dirname取得的路径名不包含最后的/basename取自于最后一个下划线后的内容。

以下是几个示例:

路径dirnamebasename
/usr/lib/usrlib
/usr//usr
usr.usr
...
.....

通过这两个函数我们就可以写出属于我们自己的dirname和basename命令了:

#include <libgen.h>
#include <stdio.h>

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Usage: %s path ...\n", argv[0]);
        return 0;
    }

    for (int i = 1; i < argc; i++) {
        printf("%s\n", dirname(argv[i])); // basename直接替换这里的函数即可
    }

    return 0;
}

编译运行:

> gcc dirname.c -o dirname
> ./dirname /usr/local /data/
/usr
/data

实际上linux也内置了dirnamebasename两个命令:

> basename /usr/lib/libDeployPkg.so.0 
libDeployPkg.so.0
> dirname /usr/local/ /data
/usr
/