介绍
本文记录的是2020.5月份应聘蔚来汽车linux 开发岗位的经历。
BOSS 岗位薪资:30K ~60K
总结:本次岗位应聘总共经历了4次面试,三次电话面试,一次视频面试。总体感觉难度中等,但是由于个人在最后一面视频面试中比较紧张,并且手写算法比较生疏,导致表现不如意,很遗憾最终没能进入该公司。虽然两个月后HR又请我再次面试,但是我已经入职新公司,并且与新同事相处愉快,就婉拒了。
希望本文能够帮助有需要的朋友,还请关注、点赞、收藏三连~~~
面试内容
1. 进程间通信有哪些?区别是什么?共享内存为什么效率会更高?
答:常用的进程间的通信方式有7种。消息队列,共享内存,管道,信号量,socket,FIFO,信号。
管道也称为无名管道。是一种半双工的通信方式,数据只能单向流动。而且只能在具有亲缘关系的进程中使用,因此一般结合fork使用。使用流程:
- 父进程创建管道,得到两个⽂件描述符指向管道的两端
- 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
- 父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
#include #include #include #include int main() { int fd[2]; int ret = pipe(fd); //fd[0] 为读打开,fd[1] 为写打开。 if (ret == -1) { perror(”pipe error\n”); return 1; } pid_t id = fork(); if (id == 0) {//child int i = 0; close(fd[0]); char *child = “I am child!”; while (i<5) { write(fd[1], child, strlen(child) + 1); sleep(2); i++; } } else if (id>0) {//father close(fd[1]); char msg[100]; int j = 0; while (j<5) { memset(msg,’\0’,sizeof(msg)); ssize_t s = read(fd[0], msg, sizeof(msg)); if (s>0) { msg[s - 1] = ’\0’; } printf(”%s\n”, msg); j++; } } else {//error perror(”fork error\n”); return 2; } return 0; } |
注意:
- 当所有读通道关闭时,所有的写通道都是不合法的,否则向管道写入数据的进程将会收到内核传来的错误SIGPIPE信号
- 当所有的写通道关闭时,在所有数据全部读完之后,read 返回0。否则只要有一个写通道未关闭,read就会阻塞。
- PIPE_BUFF 规定了内核的管道缓冲区大小。当缓冲区满了之后,写进程就会一直阻塞。
- 当有多个读进程时,写入管道的数据是随机被读进程获取的,一旦被读取,其它的读进程就无法读书数据了。
FIFO也被称为有名管道。它也是一种半双工的通信方式,但它允许无亲缘关系进程间的通信。FIFO其实是一种文件类型,使用步骤:
- 创建一个FIFO文件,这个FIFO文件是所有进程都知道。int mkfifo(const char *path, mode_t mode);
- 各进程open FIFO文件之后,通过文件操作进行通信。int mkfifoat(int fd, const char *path, mode_t mode);
代码示例:
//fifo_w.c #include #include #include #include #include #include #include #include int main() { int fd = 0; int len = 0; int ret; char buf[1024] = { 0 }; mkfifo("./test",0664); //只写方式打开test fd = open("./test", O_WRONLY); if (fd < 0) { perror("open error"); exit(1); } puts("open fifo write"); //向FIFO写入数据 while ((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0) { write(fd, buf, len); } close(fd); return 0; }
//fifo_r.c #include #include #include #include #include #include #include #include
int main() { int fd = 0; int len = 0; char buf[1024] = { 0 }; //只读打开test fd = open("./test", O_RDONLY); if (fd < 0) { perror("open error"); exit(1); } puts("open fifo read");
//从管道中读取数据 while ((len = read(fd, buf, sizeof(buf))) > 0) { write(STDOUT_FILENO, buf, len); }
//如果read返回0,说明读到文件末尾或对端已关闭 if (len == 0) { puts("peer is close or file end"); } else { puts("read error"); }
close(fd); return 0; } |
思考:FIFO文件和普通文件有何不同?
- FIFO 文件,为我们做了同步,阻塞,通知等操作。
- FIFO 管道是在磁盘中有对应的节点,但没有数据块。 无名管道是在内存中的,不可见。
信号用于通知某个事件已经发生。比如截取ctrl+c (SIGINT 终止信号)信号等。
注册信号处理函数 sighandler_t signal(int signum, sighandler_t handler); 功能:信号处理函数 参数:signum:要处理的信号//不能是SIGKILL和SIGSTOP handler:SIG_IGN:忽略该信号。 SIG_DFL:采用系统默认方式处理信号。 自定义的信号处理函数指针 返回值:成功:设置之前的信号处理方式;失败:SIG_ERR
发送信号函数 int kill(pid_t pid, int sig); 功能:信号发送 参数:pid:指定进程 sig:要发送的信号 返回值:成功 0;失败 -1 |
消息队列是消息的链接表,存储在内核中,有消息队列标识符。
#include int msgget(key_t key, int flag); msgget用于创建一个新的消息队列或打开一个现有队列
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag); msgsnd用于将新消息添加到队列尾端
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag); msgrcv用于从队列中去消息
并不一定以先进先出的顺序取消息,可以按照消息的类型字段取消息。 |
共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的IPC。
在多个进程同步访问一个给定存储区时,若服务器进程正在将数据放入存储区,则在它做完之前,客户进程不应该去取这些数据。
#include int shmget(key_t key, size_t size, int flag); shmget函数获得一个共享存储标识符:
int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmctl函数对共享存储段执行多种操作:
void *shmat(int shmid, const void *addr, int flag); 一旦创建了一个共享存储段,进程就可以调用shmat函数将其连接到它的地址空间中
int shmdt(const void *addr); 当对共享存储段的操作已经结束时,调用shmdt函数与该段分离,但这并不会删除标 识符及其相关的数据结构。直至某个进程(一般是服务器进程)调用shmctl特地删除它为止: |
优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。
缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。
信号量它是一个计数器,用于为多个进程提供共享数据对象的访问.。通常用于共享内存的同步访问,临界资源的同步访问。
#include int semget(key_t key, int nsems, int flag); 当我们想使用信号量时,首先通过函数semget来获取一个信号量ID
int semctl(int semid, int semnum, int cmd, ... /* union semun arg*/); semctl函数包含了多种信号量操作
int semop(int semid, struct sembuf semoparray[], size_t nops); semop函数自动执行信号量集合上的操作数组 |
1.1 应用场景
- 如果用户传递的信息较少,或者只是为了出发某些行为。信号是一种简洁有效的通信方式。但若是进程间要求传递的信息量较大或者存在数据交换的要求,就需要考虑别的通信方式了。
- 无名管道与有名管道的区别在于单向通信以及有关联的进程。
- 消息队列允许任意进程通过共享队列来进行进程间通信。并由系统调用函数来实现消息发送和接收之间的同步。从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用相对方便。
但是消息队列中信息的复制需要耗费CPU时间,不适宜信息量大或频繁操作的场合。
- 消息队列与管道方式的区别在于,消息队列可以实现多对多,并需要在内存中实现,而管道可以在内存或磁盘上实现。
- 共享内存无须复制,信息量大是其最大的优势。但是需要考虑同步问题。
1.2 为什么共享内存进行两次内存拷贝?其他IPC进行四次拷贝
共享内存是一种高效的IPC机制。它允许两个或多个进程共享一块内存空间,进程可以直接读写这块内存,而无需调用操作系统的任何数据传输服务
在共享内存模型中,数据通常只需要经历两次拷贝:
1. 从发送进程的用户空间拷贝到共享内存区域: 这一步涉及将数据从发送进程的用户空间拷贝到共享的内存区域。
2. 从共享内存区域拷贝到接收进程的用户空间: 接收进程从共享内存区域读取数据并拷贝到它自己的用户空间。
消息队列和管道是另一种形式的IPC,它们允许进程通过操作系统内核进行数据交换
在消息队列和管道中,数据通常需要经历四次拷贝:
1. 从发送进程的用户空间拷贝到内核空间:首先,数据从发送进程的用户空拷贝到操作系统内核空间。
2. 从内核空间拷贝到内核的缓冲区:接着,数据在内核内部从一个位置拷贝到另一个位置,通常是内核的缓冲区
3. 从内核的缓冲区拷贝回内核空间的另一部分: 当接收进程准备好接收数据时,数据从内核的缓冲区拷贝到内核空间的另一部分,以便传输给接收进程
4. 从内核空间拷贝到接收进程的用户空间: 最后,数据从内核空间拷贝到接收进程的用户空间。
2、线程间通信有哪些?区别是什么?
线程于进程之间的区别在于同一进程中的所有线程共享地址空间。它并不需要像进程间通信一样需要系统调用。线程间通信,使用全局变量即可。但是要做好共享资源的同步以及互斥,保护好共享资源。
线程同步和互斥的方式
互斥锁提供了以排他方式防止数据结构被并发修改的方法
- 初始化锁int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);或pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 阻塞加锁int pthread_mutex_lock(pthread_mutex *mutex);若锁已经被占用,挂起等待
- 非阻塞加锁int pthread_mutex_trylock( pthread_mutex_t *mutex);该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待
- 解锁int pthread_mutex_unlock(pthread_mutex *mutex);要求锁处于lock状态
- 锁销毁int pthread_mutex_destroy(pthread_mutex *mutex);要求锁此时处于unlock状态
#include #include #include #include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int gn;
void* thread(void *arg) { printf("thread's ID is %d\n",pthread_self()); pthread_mutex_lock(&mutex); gn = 12; printf("Now gn = %d\n",gn); pthread_mutex_unlock(&mutex); return NULL; }
int main() { pthread_t id; printf("main thread's ID is %d\n",pthread_self()); gn = 3; printf("In main func, gn = %d\n",gn); if (!pthread_create(&id, NULL, thread, NULL)) { printf("Create thread success!\n"); } else { printf("Create thread failed!\n"); } pthread_join(id, NULL); pthread_mutex_destroy(&mutex); return 0; } |
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
- 如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样就可以多个线程并行操作。但这个时候,如果再进行写锁加锁就会发生阻塞,写锁请求阻塞后,后面如果继续有读锁来请求,这些后来的读锁都会被阻塞!这样避免了读锁长期占用资源,防止写锁饥饿!
- 如果一个线程用写锁锁住了临界区,那么其他线程不管是读锁还是写锁都会发生阻塞!
- 初始化int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
- 读写加解锁int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);、int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);、int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);、int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);、int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);、int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);、int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
- 销毁锁int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
#include #include #include #include #include
/* 初始化读写锁 */ pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; /* 全局资源 */ int global_num = 10;
void err_exit(const char *err_msg) { printf("error:%s\n", err_msg); exit(1); }
/* 读锁线程函数 */ void *thread_read_lock(void *arg) { char *pthr_name = (char *)arg;
while (1) { /* 读加锁 */ pthread_rwlock_rdlock(&rwlock);
printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num); sleep(1); printf("线程%s离开临界区...\n", pthr_name);
/* 读解锁 */ pthread_rwlock_unlock(&rwlock);
sleep(1); }
return NULL; }
/* 写锁线程函数 */ void *thread_write_lock(void *arg) { char *pthr_name = (char *)arg;
while (1) { /* 写加锁 */ pthread_rwlock_wrlock(&rwlock);
/* 写操作 */ global_num++; printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num); sleep(1); printf("线程%s离开临界区...\n", pthr_name);
/* 写解锁 */ pthread_rwlock_unlock(&rwlock);
sleep(2); }
return NULL; }
int main(void) { pthread_t tid_read_1, tid_read_2, tid_write_1, tid_write_2;
/* 创建4个线程,2个读,2个写 */ if (pthread_create(&tid_read_1, NULL, thread_read_lock, "read_1") != 0) err_exit("create tid_read_1");
if (pthread_create(&tid_read_2, NULL, thread_read_lock, "read_2") != 0) err_exit("create tid_read_2");
if (pthread_create(&tid_write_1, NULL, thread_write_lock, "write_1") != 0) err_exit("create tid_write_1");
if (pthread_create(&tid_write_2, NULL, thread_write_lock, "write_2") != 0) err_exit("create tid_write_2");
/* 随便等待一个线程,防止main结束 */ if (pthread_join(tid_read_1, NULL) != 0) err_exit("pthread_join()");
return 0; } |
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件变量的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
- 初始化条件变量int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
- 等待函数int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
- 无条件等待int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
- 计时等待int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
- 激发条件int pthread_cond_signal(pthread_cond_t *cond);
- 激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)int pthread_cond_broadcast(pthread_cond_t *cond);激活所有等待线程
- 销毁条件变量int pthread_cond_destroy(pthread_cond_t *cond);
#include #include #include "stdlib.h" #include "unistd.h"
pthread_mutex_t mutex; pthread_cond_t cond;
void hander(void *arg) { free(arg); (void)pthread_mutex_unlock(&mutex); }
void *thread1(void *arg) { pthread_cleanup_push(hander, &mutex); while (1) { printf("thread1 is running\n"); pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex); printf("thread1 applied the condition\n"); pthread_mutex_unlock(&mutex); sleep(4); } pthread_cleanup_pop(0); }
void *thread2(void *arg) { while (1) { printf("thread2 is running\n"); pthread_mutex_lock(&mutex); pthread_cond_wait(&cond,&mutex); printf("thread2 applied the condition\n"); pthread_mutex_unlock(&mutex); sleep(1); } }
int main() { pthread_t thid1,thid2; printf("condition variable study!\n"); pthread_mutex_init(&mutex,NULL); pthread_cond_init(&cond,NULL); pthread_create(&thid1,NULL,thread1,NULL); pthread_create(&thid2,NULL,thread2,NULL);
sleep(1); do { pthread_cond_signal(&cond); } while(1);
sleep(20); pthread_exit(0); return 0; } |
3. 同主机中两个进程之间的socket 文件传输,需要几次内存拷贝?不同主机之间的socket传输呢?
不同进程之间socket 通信需要进行四次拷贝。
第一次:文件操作, read 磁盘中的文件,拷贝到进程的用户空间内存中。
第二次:socket发送 ,write 用户空间中内存传到内核空间内存中。
第三次:接收进程socket read。将内核空间中内存,拷贝到用户空间的内存中
第四次:接收进程,通过write 写入磁盘文件。
不同主机之间传输需要6次拷贝,多了两次 内核-> 网卡, 网卡-> 内核 拷贝。